com.openhtmltopdf.layout.Layer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openhtmltopdf-core Show documentation
Show all versions of openhtmltopdf-core Show documentation
Open HTML to PDF is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code.
/*
* {{{ header & license
* Copyright (c) 2005 Wisconsin Court System
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
package com.openhtmltopdf.layout;
import com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.constants.MarginBoxName;
import com.openhtmltopdf.css.constants.PageElementPosition;
import com.openhtmltopdf.css.newmatch.PageInfo;
import com.openhtmltopdf.css.parser.CSSPrimitiveValue;
import com.openhtmltopdf.css.parser.PropertyValue;
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.CssContext;
import com.openhtmltopdf.css.style.EmptyStyle;
import com.openhtmltopdf.css.style.FSDerivedValue;
import com.openhtmltopdf.css.style.derived.ListValue;
import com.openhtmltopdf.css.style.derived.RectPropertySet;
import com.openhtmltopdf.newtable.CollapsedBorderValue;
import com.openhtmltopdf.newtable.TableBox;
import com.openhtmltopdf.newtable.TableCellBox;
import com.openhtmltopdf.render.*;
import com.openhtmltopdf.render.displaylist.TransformCreator;
import com.openhtmltopdf.util.LogMessageId;
import com.openhtmltopdf.util.XRLog;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.util.*;
import java.util.List;
import java.util.logging.Level;
/**
* All positioned content as well as content with an overflow value other
* than visible creates a layer. Layers which define stacking contexts
* provide the entry for rendering the box tree to an output device. The main
* purpose of this class is to provide an implementation of Appendix E of the
* spec, but it also provides additional utility services including page
* management and mapping boxes to coordinates (for e.g. links). When
* rendering to a paged output device, the layer is also responsible for laying
* out absolute content (which is layed out after its containing block has
* completed layout).
*/
public class Layer {
public static final short PAGED_MODE_SCREEN = 1;
public static final short PAGED_MODE_PRINT = 2;
private Layer _parent;
private boolean _stackingContext;
private List _children;
private Box _master;
private Box _end;
private List _floats;
private boolean _fixedBackground;
private boolean _inline;
private boolean _requiresLayout;
private List _pages;
private PageBox _lastRequestedPage = null;
private Set _pageSequences;
private List _sortedPageSequences;
private Map> _runningBlocks;
private Box _selectionStart;
private Box _selectionEnd;
private int _selectionStartX;
private int _selectionStartY;
private int _selectionEndX;
private int _selectionEndY;
private boolean _forDeletion;
private boolean _hasFixedAncester;
/**
* @see {@link #getCurrentTransformMatrix()}
*/
private AffineTransform _ctm;
private final boolean _hasLocalTransform;
/**
* Creates the root layer.
*/
public Layer(Box master, CssContext c) {
this(null, master, c);
setStackingContext(true);
}
/**
* Creates a child layer.
*/
public Layer(Layer parent, Box master, CssContext c) {
_parent = parent;
_master = master;
setStackingContext(
(master.getStyle().isPositioned() && !master.getStyle().isAutoZIndex()) ||
(!master.getStyle().isIdent(CSSName.TRANSFORM, IdentValue.NONE)));
master.setLayer(this);
master.setContainingLayer(this);
_hasLocalTransform = !master.getStyle().isIdent(CSSName.TRANSFORM, IdentValue.NONE);
_hasFixedAncester = (parent != null && parent._hasFixedAncester) || master.getStyle().isFixed();
}
/**
* Recursively propagates the transformation matrix. This must be done after layout of the master
* box and its children as this method relies on the box width and height for relative units in the
* transforms and transform origins.
*/
public void propagateCurrentTransformationMatrix(CssContext c) {
AffineTransform parentCtm = _parent == null ? null : _parent._ctm;
_ctm = _hasLocalTransform ?
TransformCreator.createDocumentCoordinatesTransform(getMaster(), c, parentCtm) : parentCtm;
for (Layer child : getChildren()) {
child.propagateCurrentTransformationMatrix(c);
}
}
/**
* The document coordinates current transform, this is cumulative from layer to child layer.
* May be null, if identity transform is in effect.
* Used to check if a box belonging to this layer sits on a particular page after the
* transform is applied.
* This method can only be used after {@link #propagateCurrentTransformationMatrix(CssContext)} has been
* called on the root layer.
* @return null or affine transform.
*/
public AffineTransform getCurrentTransformMatrix() {
return _ctm;
}
public boolean hasLocalTransform() {
return _hasLocalTransform;
}
public void setForDeletion(boolean forDeletion) {
this._forDeletion = forDeletion;
}
public boolean isForDeletion() {
return this._forDeletion;
}
public boolean hasFixedAncester() {
return _hasFixedAncester;
}
public Layer getParent() {
return _parent;
}
public boolean isStackingContext() {
return _stackingContext;
}
public void setStackingContext(boolean stackingContext) {
_stackingContext = stackingContext;
}
public int getZIndex() {
if (_master.getStyle().isIdent(CSSName.Z_INDEX, IdentValue.AUTO)) {
return 0;
}
return (int) _master.getStyle().asFloat(CSSName.Z_INDEX);
}
public boolean isZIndexAuto() {
return _master.getStyle().isIdent(CSSName.Z_INDEX, IdentValue.AUTO);
}
public Box getMaster() {
return _master;
}
public void addChild(Layer layer) {
if (_children == null) {
_children = new ArrayList<>();
}
_children.add(layer);
}
public static PageBox createPageBox(CssContext c, String pseudoPage) {
PageBox result = new PageBox();
String pageName = null;
// HACK We only create pages during layout, but the OutputDevice
// queries page positions and since pages are created lazily, changing
// this method to use LayoutContext is tricky
if (c instanceof LayoutContext) {
pageName = ((LayoutContext)c).getPageName();
}
PageInfo pageInfo = c.getCss().getPageStyle(pageName, pseudoPage);
result.setPageInfo(pageInfo);
CalculatedStyle cs = new EmptyStyle().deriveStyle(pageInfo.getPageStyle());
result.setStyle(cs);
result.setOuterPageWidth(result.getWidth(c));
return result;
}
/**
* FIXME: Only used when we reset a box, so trying to remove at sometime in the future.
*/
public void removeFloat(BlockBox floater) {
if (_floats != null) {
_floats.remove(floater);
}
}
@Deprecated // We are moving painting out of Layer to either DisplayListPainter or SimplePainter.
private void paintFloats(RenderingContext c) {
if (_floats != null) {
for (int i = _floats.size() - 1; i >= 0; i--) {
BlockBox floater = _floats.get(i);
paintAsLayer(c, floater);
}
}
}
@Deprecated
private void paintLayers(RenderingContext c, List layers) {
for (Layer layer : layers) {
layer.paint(c);
}
}
public static final int POSITIVE = 1;
public static final int ZERO = 2;
public static final int NEGATIVE = 3;
public static final int AUTO = 4;
public void addFloat(BlockBox floater, BlockFormattingContext bfc) {
if (_floats == null) {
_floats = new ArrayList<>();
}
_floats.add(floater);
floater.getFloatedBoxData().setDrawingLayer(this);
}
/**
* Called recusively to collect all descendant layers in a stacking context so they can be painted in correct order.
* Those descendants that are under their own stacking contexts are excluded.
* @param which NEGATIVE ZERO POSITIVE AUTO corresponding to z-index property.
* @return
*/
public List collectLayers(int which) {
List result = new ArrayList<>();
List children = getChildren();
result.addAll(getStackingContextLayers(which));
for (Layer child : children) {
if (! child.isStackingContext()) {
if (child.isForDeletion()) {
// Do nothing...
} else if (which == AUTO && child.isZIndexAuto()) {
result.add(child);
} else if (which == NEGATIVE && child.getZIndex() < 0) {
result.add(child);
} else if (which == POSITIVE && child.getZIndex() > 0) {
result.add(child);
} else if (which == ZERO && !child.isZIndexAuto() && child.getZIndex() == 0) {
result.add(child);
}
result.addAll(child.collectLayers(which));
}
}
return result;
}
private List getStackingContextLayers(int which) {
List result = new ArrayList<>();
List children = getChildren();
for (Layer target : children) {
if (target.isForDeletion()) {
// Do nothing...
} else if (target.isStackingContext()) {
if (!target.isZIndexAuto()) {
int zIndex = target.getZIndex();
if (which == NEGATIVE && zIndex < 0) {
result.add(target);
} else if (which == POSITIVE && zIndex > 0) {
result.add(target);
} else if (which == ZERO && zIndex == 0) {
result.add(target);
}
} else if (which == AUTO) {
result.add(target);
}
}
}
return result;
}
public List getSortedLayers(int which) {
List result = collectLayers(which);
Collections.sort(result, (l1, l2) -> l1.getZIndex() - l2.getZIndex());
return result;
}
@Deprecated
private void paintBackgroundsAndBorders(
RenderingContext c, List blocks,
Map> collapsedTableBorders, BoxRangeLists rangeLists) {
BoxRangeHelper helper = new BoxRangeHelper(c.getOutputDevice(), rangeLists.getBlock());
for (int i = 0; i < blocks.size(); i++) {
helper.popClipRegions(c, i);
BlockBox box = (BlockBox)blocks.get(i);
box.paintBackground(c);
box.paintBorder(c);
if (c.debugDrawBoxes()) {
box.paintDebugOutline(c);
}
if (collapsedTableBorders != null && box instanceof TableCellBox) {
TableCellBox cell = (TableCellBox)box;
if (cell.hasCollapsedPaintingBorder()) {
List borders = collapsedTableBorders.get(cell);
if (borders != null) {
paintCollapsedTableBorders(c, borders);
}
}
}
helper.pushClipRegion(c, i);
}
helper.popClipRegions(c, blocks.size());
}
@Deprecated // We no longer support interactive or selection.
private void paintSelection(RenderingContext c, List lines) {
if (c.getOutputDevice().isSupportsSelection()) {
for (Iterator i = lines.iterator(); i.hasNext();) {
Box box = i.next();
if (box instanceof InlineLayoutBox) {
((InlineLayoutBox)box).paintSelection(c);
}
}
}
}
public Dimension getPaintingDimension(LayoutContext c) {
return calcPaintingDimension(c).getOuterMarginCorner();
}
@Deprecated
private void paintInlineContent(RenderingContext c, List lines, BoxRangeLists rangeLists) {
BoxRangeHelper helper = new BoxRangeHelper(
c.getOutputDevice(), rangeLists.getInline());
for (int i = 0; i < lines.size(); i++) {
helper.popClipRegions(c, i);
helper.pushClipRegion(c, i);
if (lines.get(i) instanceof InlinePaintable) {
((InlinePaintable) lines.get(i)).paintInline(c);
}
}
helper.popClipRegions(c, lines.size());
}
@Deprecated
public void paint(RenderingContext c) {
if (getMaster().getStyle().isFixed()) {
positionFixedLayer(c);
}
List inverse = null;
if (isRootLayer()) {
getMaster().paintRootElementBackground(c);
}
if (! isInline() && ((BlockBox)getMaster()).isReplaced()) {
inverse = applyTranform(c, getMaster());
paintLayerBackgroundAndBorder(c);
paintReplacedElement(c, (BlockBox)getMaster());
c.getOutputDevice().popTransforms(inverse);
} else {
BoxRangeLists rangeLists = new BoxRangeLists();
List blocks = new ArrayList<>();
List lines = new ArrayList<>();
BoxCollector collector = new BoxCollector();
collector.collect(c, c.getOutputDevice().getClip(), this, blocks, lines, rangeLists);
inverse = applyTranform(c, getMaster());
if (! isInline()) {
paintLayerBackgroundAndBorder(c);
if (c.debugDrawBoxes()) {
((BlockBox)getMaster()).paintDebugOutline(c);
}
}
if (isRootLayer() || isStackingContext()) {
paintLayers(c, getSortedLayers(NEGATIVE));
}
Map> collapsedTableBorders = collectCollapsedTableBorders(c, blocks);
paintBackgroundsAndBorders(c, blocks, collapsedTableBorders, rangeLists);
paintFloats(c);
paintListMarkers(c, blocks, rangeLists);
paintInlineContent(c, lines, rangeLists);
paintReplacedElements(c, blocks, rangeLists);
paintSelection(c, lines); // XXX do only when there is a selection
if (isRootLayer() || isStackingContext()) {
paintLayers(c, collectLayers(AUTO));
// TODO z-index: 0 layers should be painted atomically
paintLayers(c, getSortedLayers(ZERO));
paintLayers(c, getSortedLayers(POSITIVE));
}
c.getOutputDevice().popTransforms(inverse);
}
}
@Deprecated // Moving to TransformCreator
private float convertAngleToRadians(PropertyValue param) {
if (param.getPrimitiveType() == CSSPrimitiveValue.CSS_DEG) {
return (float) Math.toRadians(param.getFloatValue());
} else if (param.getPrimitiveType() == CSSPrimitiveValue.CSS_RAD) {
return param.getFloatValue();
} else { // if (param.getPrimitiveType() == CSSPrimitiveValue.CSS_GRAD)
return (float) (param.getFloatValue() * (Math.PI / 200));
}
}
public List getFloats() {
return _floats == null ? Collections.emptyList() : _floats;
}
/**
* Applies the transforms specified for the box and returns a list of inverse transforms that should be
* applied once the transformed element has been output.
*/
@Deprecated
protected List applyTranform(RenderingContext c, Box box) {
FSDerivedValue transforms = box.getStyle().valueByName(CSSName.TRANSFORM);
if (transforms.isIdent() && transforms.asIdentValue() == IdentValue.NONE)
return Collections.emptyList();
// By default the transform point is the lower left of the page, so we need to
// translate to correctly apply transform.
float relOriginX = box.getStyle().getFloatPropertyProportionalWidth(CSSName.FS_TRANSFORM_ORIGIN_X,
box.getWidth(), c);
float relOriginY = box.getStyle().getFloatPropertyProportionalHeight(CSSName.FS_TRANSFORM_ORIGIN_Y,
box.getHeight(), c);
float flipFactor = c.getOutputDevice().isPDF() ? -1 : 1;
float absTranslateX = relOriginX + box.getAbsX();
float absTranslateY = relOriginY + box.getAbsY();
float relTranslateX = absTranslateX - c.getOutputDevice().getAbsoluteTransformOriginX();
float relTranslateY = absTranslateY - c.getOutputDevice().getAbsoluteTransformOriginY();
/*
* We must handle the page margin in the PDF case.
*/
if (c.getOutputDevice().isPDF()) {
RectPropertySet margin = c.getPage().getMargin(c);
relTranslateX += margin.left();
relTranslateY += margin.top();
/*
* We must apply the top/bottom margins from the previous pages, otherwise
* our transform center is wrong.
*/
for (int i = 0; i < c.getPageNo() && i < getPages().size(); i++) {
RectPropertySet prevMargin = getPages().get(i).getMargin(c);
relTranslateY += prevMargin.top() + prevMargin.bottom();
}
MarginBoxName[] marginBoxNames = c.getPage().getCurrentMarginBoxNames();
if (marginBoxNames != null) {
boolean isLeft = false, isTop = false, isRight = false, isTopRight = false, isTopLeft = true,
isBottom = false, isBottomRight = false, isBottomLeft = false;
for (MarginBoxName name : marginBoxNames) {
if (name == MarginBoxName.LEFT_TOP || name == MarginBoxName.LEFT_MIDDLE
|| name == MarginBoxName.LEFT_BOTTOM)
isLeft = true;
if (name == MarginBoxName.TOP_LEFT || name == MarginBoxName.TOP_CENTER
|| name == MarginBoxName.TOP_RIGHT)
isTop = true;
if (name == MarginBoxName.BOTTOM_LEFT || name == MarginBoxName.BOTTOM_CENTER
|| name == MarginBoxName.BOTTOM_RIGHT)
isBottom = true;
if (name == MarginBoxName.TOP_LEFT_CORNER)
isTopLeft = true;
if (name == MarginBoxName.TOP_RIGHT_CORNER)
isTopRight = true;
if (name == MarginBoxName.BOTTOM_LEFT_CORNER)
isBottomLeft = true;
if (name == MarginBoxName.BOTTOM_RIGHT_CORNER)
isBottomRight = true;
}
if (isLeft)
relTranslateX -= margin.left();
if (isTop )
relTranslateY -= margin.top();
if( isBottom )
relTranslateY -= margin.top()+ margin.bottom();
if (isTopLeft) {
relTranslateX -= margin.left();
relTranslateY -= margin.top();
}
if (isTopRight) {
relTranslateX -= margin.left();
relTranslateY -= margin.top();
}
if (isRight) {
relTranslateY -= margin.top();
relTranslateX -= margin.left() + margin.right();
}
if (isBottom) {
//relTranslateX -= margin.left();
relTranslateY -= margin.top() + margin.bottom();
}
if (isBottomLeft) {
//relTranslateX -= margin.left();
//relTranslateY -= margin.top() + margin.bottom();
}
if (isBottomRight) {
relTranslateX -= margin.left();
relTranslateY -= margin.top() + margin.bottom();
}
}
}
List transformList = (List) ((ListValue) transforms).getValues();
List resultTransforms = new ArrayList<>();
AffineTransform translateToOrigin = AffineTransform.getTranslateInstance(relTranslateX, relTranslateY);
AffineTransform translateBackFromOrigin = AffineTransform.getTranslateInstance(-relTranslateX, -relTranslateY);
resultTransforms.add(translateToOrigin);
applyTransformFunctions(flipFactor, transformList, resultTransforms);
resultTransforms.add(translateBackFromOrigin);
return c.getOutputDevice().pushTransforms(resultTransforms);
}
@Deprecated
private void applyTransformFunctions(float flipFactor, List transformList, List resultTransforms) {
for (PropertyValue transform : transformList) {
String fName = transform.getFunction().getName();
List params = transform.getFunction().getParameters();
if ("rotate".equalsIgnoreCase(fName)) {
float radians = flipFactor * this.convertAngleToRadians(params.get(0));
resultTransforms.add(AffineTransform.getRotateInstance(radians));
} else if ("scale".equalsIgnoreCase(fName) || "scalex".equalsIgnoreCase(fName)
|| "scaley".equalsIgnoreCase(fName)) {
float scaleX = params.get(0).getFloatValue();
float scaleY = params.get(0).getFloatValue();
if (params.size() > 1)
scaleY = params.get(1).getFloatValue();
if ("scalex".equalsIgnoreCase(fName))
scaleY = 1;
if ("scaley".equalsIgnoreCase(fName))
scaleX = 1;
resultTransforms.add(AffineTransform.getScaleInstance(scaleX, scaleY));
} else if ("skew".equalsIgnoreCase(fName)) {
float radiansX = flipFactor * this.convertAngleToRadians(params.get(0));
float radiansY = 0;
if (params.size() > 1)
radiansY = this.convertAngleToRadians(params.get(1));
resultTransforms.add(AffineTransform.getShearInstance(Math.tan(radiansX), Math.tan(radiansY)));
} else if ("skewx".equalsIgnoreCase(fName)) {
float radians = flipFactor * this.convertAngleToRadians(params.get(0));
resultTransforms.add(AffineTransform.getShearInstance(Math.tan(radians), 0));
} else if ("skewy".equalsIgnoreCase(fName)) {
float radians = flipFactor * this.convertAngleToRadians(params.get(0));
resultTransforms.add(AffineTransform.getShearInstance(0, Math.tan(radians)));
} else if ("matrix".equalsIgnoreCase(fName)) {
resultTransforms.add(new AffineTransform(params.get(0).getFloatValue(), params.get(1).getFloatValue(),
params.get(2).getFloatValue(), params.get(3).getFloatValue(),
params.get(4).getFloatValue(), params.get(5).getFloatValue()));
} else if ("translate".equalsIgnoreCase(fName)) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.LAYOUT_FUNCTION_NOT_IMPLEMENTED, "translate");
} else if ("translateX".equalsIgnoreCase(fName)) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.LAYOUT_FUNCTION_NOT_IMPLEMENTED, "translateX");
} else if ("translateY".equalsIgnoreCase(fName)) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.LAYOUT_FUNCTION_NOT_IMPLEMENTED, "translateY");
}
}
}
@Deprecated // Currently not using the find functionality and considering removing.
private Box find(CssContext cssCtx, int absX, int absY, List layers, boolean findAnonymous) {
Box result = null;
// Work backwards since layers are painted forwards and we're looking
// for the top-most box
for (int i = layers.size()-1; i >= 0; i--) {
Layer l = layers.get(i);
result = l.find(cssCtx, absX, absY, findAnonymous);
if (result != null) {
return result;
}
}
return result;
}
@Deprecated
public Box find(CssContext cssCtx, int absX, int absY, boolean findAnonymous) {
Box result = null;
if (isRootLayer() || isStackingContext()) {
result = find(cssCtx, absX, absY, getSortedLayers(POSITIVE), findAnonymous);
if (result != null) {
return result;
}
result = find(cssCtx, absX, absY, getSortedLayers(ZERO), findAnonymous);
if (result != null) {
return result;
}
result = find(cssCtx, absX, absY, collectLayers(AUTO), findAnonymous);
if (result != null) {
return result;
}
}
for (int i = 0; i < getFloats().size(); i++) {
Box floater = getFloats().get(i);
result = floater.find(cssCtx, absX, absY, findAnonymous);
if (result != null) {
return result;
}
}
result = getMaster().find(cssCtx, absX, absY, findAnonymous);
if (result != null) {
return result;
}
if (isRootLayer() || isStackingContext()) {
result = find(cssCtx, absX, absY, getSortedLayers(NEGATIVE), findAnonymous);
if (result != null) {
return result;
}
}
return null;
}
@Deprecated
private void paintCollapsedTableBorders(RenderingContext c, List borders) {
for (Iterator i = borders.iterator(); i.hasNext(); ) {
CollapsedBorderSide border = i.next();
border.getCell().paintCollapsedBorder(c, border.getSide());
}
}
// Bit of a kludge here. We need to paint collapsed table borders according
// to priority so (for example) wider borders float to the top and aren't
// overpainted by thinner borders. This method scans the block boxes
// we're about to draw and returns a map with the last cell in a given table
// we'll paint as a key and a sorted list of borders as values. These are
// then painted after we've drawn the background for this cell.
@Deprecated
private Map> collectCollapsedTableBorders(RenderingContext c, List blocks) {
Map> cellBordersByTable = new HashMap<>();
Map triggerCellsByTable = new HashMap<>();
Set all = new HashSet<>();
for (Iterator i = blocks.iterator(); i.hasNext(); ) {
Box b = i.next();
if (b instanceof TableCellBox) {
TableCellBox cell = (TableCellBox)b;
if (cell.hasCollapsedPaintingBorder()) {
List borders = cellBordersByTable.get(cell.getTable());
if (borders == null) {
borders = new ArrayList<>();
cellBordersByTable.put(cell.getTable(), borders);
}
triggerCellsByTable.put(cell.getTable(), cell);
cell.addCollapsedBorders(all, borders);
}
}
}
if (triggerCellsByTable.size() == 0) {
return null;
} else {
Map> result = new HashMap<>();
for (Iterator i = triggerCellsByTable.values().iterator(); i.hasNext(); ) {
TableCellBox cell = i.next();
List borders = cellBordersByTable.get(cell.getTable());
Collections.sort(borders);
result.put(cell, borders);
}
return result;
}
}
@Deprecated
public void paintAsLayer(RenderingContext c, BlockBox startingPoint) {
BoxRangeLists rangeLists = new BoxRangeLists();
List blocks = new ArrayList<>();
List lines = new ArrayList<>();
BoxCollector collector = new BoxCollector();
collector.collect(c, c.getOutputDevice().getClip(),
this, startingPoint, blocks, lines, rangeLists);
Map> collapsedTableBorders = collectCollapsedTableBorders(c, blocks);
paintBackgroundsAndBorders(c, blocks, collapsedTableBorders, rangeLists);
paintListMarkers(c, blocks, rangeLists);
paintInlineContent(c, lines, rangeLists);
paintSelection(c, lines); // XXX only do when there is a selection
paintReplacedElements(c, blocks, rangeLists);
}
@Deprecated
private void paintListMarkers(RenderingContext c, List blocks, BoxRangeLists rangeLists) {
BoxRangeHelper helper = new BoxRangeHelper(c.getOutputDevice(), rangeLists.getBlock());
for (int i = 0; i < blocks.size(); i++) {
helper.popClipRegions(c, i);
if (blocks.get(i) instanceof BlockBox) {
((BlockBox) blocks.get(i)).paintListMarker(c);
}
helper.pushClipRegion(c, i);
}
helper.popClipRegions(c, blocks.size());
}
@Deprecated
private void paintReplacedElements(RenderingContext c, List blocks, BoxRangeLists rangeLists) {
BoxRangeHelper helper = new BoxRangeHelper(c.getOutputDevice(), rangeLists.getBlock());
for (int i = 0; i < blocks.size(); i++) {
helper.popClipRegions(c, i);
BlockBox box = (BlockBox)blocks.get(i);
if (box.isReplaced()) {
paintReplacedElement(c, box);
}
helper.pushClipRegion(c, i);
}
helper.popClipRegions(c, blocks.size());
}
@Deprecated
private void paintLayerBackgroundAndBorder(RenderingContext c) {
if (getMaster() instanceof BlockBox) {
BlockBox box = (BlockBox) getMaster();
box.paintBackground(c);
box.paintBorder(c);
}
}
public void positionFixedLayer(RenderingContext c) {
Rectangle rect = c.getFixedRectangle();
Box fixed = getMaster();
fixed.setX(0);
fixed.setY(0);
fixed.setAbsX(0);
fixed.setAbsY(0);
fixed.setContainingBlock(new ViewportBox(rect));
((BlockBox)fixed).positionAbsolute(c, BlockBox.POSITION_BOTH);
fixed.calcPaintingInfo(c, false);
}
public boolean isRootLayer() {
return getParent() == null && isStackingContext();
}
private void moveIfGreater(Dimension result, Dimension test) {
if (test.width > result.width) {
result.width = test.width;
}
if (test.height > result.height) {
result.height = test.height;
}
}
@Deprecated
private void paintReplacedElement(RenderingContext c, BlockBox replaced) {
Rectangle contentBounds = replaced.getContentAreaEdge(
replaced.getAbsX(), replaced.getAbsY(), c);
// Minor hack: It's inconvenient to adjust for margins, border, padding during
// layout so just do it here.
Point loc = replaced.getReplacedElement().getLocation();
if (contentBounds.x != loc.x || contentBounds.y != loc.y) {
replaced.getReplacedElement().setLocation(contentBounds.x, contentBounds.y);
}
if (! c.isInteractive() || replaced.getReplacedElement().isRequiresInteractivePaint()) {
c.getOutputDevice().paintReplacedElement(c, replaced);
}
}
public void positionChildren(LayoutContext c) {
for (Layer child : getChildren()) {
child.position(c);
}
}
private PaintingInfo calcPaintingDimension(LayoutContext c) {
getMaster().calcPaintingInfo(c, true);
PaintingInfo result = (PaintingInfo)getMaster().getPaintingInfo().copyOf();
for (Layer child : getChildren()) {
if (child.getMaster().getStyle().isFixed()) {
continue;
} else if (child.getMaster().getStyle().isAbsolute()) {
PaintingInfo info = child.calcPaintingDimension(c);
moveIfGreater(result.getOuterMarginCorner(), info.getOuterMarginCorner());
}
}
return result;
}
@Deprecated // Not used.
private boolean containsFixedLayer() {
for (Layer child : getChildren()) {
if (child.getMaster().getStyle().isFixed() || child.containsFixedLayer()) {
return true;
}
}
return false;
}
@Deprecated
public boolean containsFixedContent() {
return _fixedBackground || containsFixedLayer();
}
@Deprecated // We not longer support fixed background.
public void setFixedBackground(boolean b) {
_fixedBackground = b;
}
/**
* The resulting list should not be modified.
*/
public List getChildren() {
return _children == null ? Collections.emptyList() : _children;
}
private void remove(Layer layer) {
boolean removed = false;
if (_children != null) {
for (Iterator i = _children.iterator(); i.hasNext(); ) {
Layer child = i.next();
if (child == layer) {
removed = true;
i.remove();
break;
}
}
}
if (! removed) {
throw new RuntimeException("Could not find layer to remove");
}
}
public void detach() {
if (getParent() != null) {
getParent().remove(this);
}
setForDeletion(true);
}
public boolean isInline() {
return _inline;
}
public void setInline(boolean inline) {
_inline = inline;
}
public Box getEnd() {
return _end;
}
public void setEnd(Box end) {
_end = end;
}
public boolean isRequiresLayout() {
return _requiresLayout;
}
public void setRequiresLayout(boolean requiresLayout) {
_requiresLayout = requiresLayout;
}
public void finish(LayoutContext c) {
if (c.isPrint()) {
layoutAbsoluteChildren(c);
}
if (! isInline()) {
positionChildren(c);
}
}
private void layoutAbsoluteChildren(LayoutContext c) {
List children = getChildren();
if (children.size() > 0) {
LayoutState state = c.captureLayoutState();
for (int i = 0; i < children.size(); i++) {
Layer child = children.get(i);
boolean isFixed = child.getMaster().getStyle().isFixed();
if (child.isRequiresLayout()) {
layoutAbsoluteChild(c, child);
if (!isFixed &&
child.getMaster().getStyle().isAvoidPageBreakInside() &&
child.getMaster().crossesPageBreak(c)) {
BlockBox master = (BlockBox) child.getMaster();
master.reset(c);
master.setNeedPageClear(true);
layoutAbsoluteChild(c, child);
if (master.crossesPageBreak(c)) {
master.reset(c);
layoutAbsoluteChild(c, child);
}
}
child.setRequiresLayout(false);
child.finish(c);
if (!isFixed) {
c.getRootLayer().ensureHasPage(c, child.getMaster());
}
}
}
c.restoreLayoutState(state);
}
}
private void position(LayoutContext c) {
if (getMaster().getStyle().isAbsolute() && ! c.isPrint()) {
((BlockBox)getMaster()).positionAbsolute(c, BlockBox.POSITION_BOTH);
} else if (getMaster().getStyle().isRelative() &&
(isInline() || ((BlockBox)getMaster()).isInline())) {
getMaster().positionRelative(c);
if (! isInline()) {
getMaster().calcCanvasLocation();
getMaster().calcChildLocations();
}
}
}
public List getPages() {
if (_pages == null)
return _parent == null ? Collections. emptyList() : _parent.getPages();
return _pages;
}
public void setPages(List pages) {
_pages = pages;
}
public boolean isLastPage(PageBox pageBox) {
return _pages.get(_pages.size()-1) == pageBox;
}
private void layoutAbsoluteChild(LayoutContext c, Layer child) {
BlockBox master = (BlockBox)child.getMaster();
if (child.getMaster().getStyle().isBottomAuto()) {
// Set top, left
master.positionAbsolute(c, BlockBox.POSITION_BOTH);
master.positionAbsoluteOnPage(c);
c.reInit(true);
((BlockBox)child.getMaster()).layout(c);
// Set right
master.positionAbsolute(c, BlockBox.POSITION_HORIZONTALLY);
} else {
// FIXME Not right in the face of pagination, but what
// to do? Not sure if just laying out and positioning
// repeatedly will converge on the correct position,
// so just guess for now
c.reInit(true);
master.layout(c);
BoxDimensions before = master.getBoxDimensions();
master.reset(c);
BoxDimensions after = master.getBoxDimensions();
master.setBoxDimensions(before);
master.positionAbsolute(c, BlockBox.POSITION_BOTH);
master.positionAbsoluteOnPage(c);
master.setBoxDimensions(after);
c.reInit(true);
((BlockBox)child.getMaster()).layout(c);
}
}
public void removeLastPage() {
PageBox pageBox = _pages.remove(_pages.size()-1);
if (pageBox == getLastRequestedPage()) {
setLastRequestedPage(null);
}
}
public void addPage(CssContext c) {
String pseudoPage = null;
if (_pages == null) {
_pages = new ArrayList<>();
}
List pages = getPages();
if (pages.size() == 0) {
pseudoPage = "first";
} else if (pages.size() % 2 == 0) {
pseudoPage = "right";
} else {
pseudoPage = "left";
}
PageBox pageBox = createPageBox(c, pseudoPage);
if (pages.size() == 0) {
pageBox.setTopAndBottom(c, 0);
} else {
PageBox previous = pages.get(pages.size()-1);
pageBox.setTopAndBottom(c, previous.getBottom());
}
pageBox.setPageNo(pages.size());
pages.add(pageBox);
}
/**
* Returns the page box for a Y position.
* If the y position is less than 0 then the first page will
* be returned if available.
* Returns null if there are no pages available or absY
* is past the last page.
*/
public PageBox getFirstPage(CssContext c, int absY) {
PageBox page = getPage(c, absY);
if (page == null && absY < 0) {
List pages = getPages();
if (!pages.isEmpty()) {
return pages.get(0);
}
}
return page;
}
public PageBox getFirstPage(CssContext c, Box box) {
if (box instanceof LineBox) {
LineBox lb = (LineBox) box;
return getPage(c, lb.getMinPaintingTop());
}
return getPage(c, box.getAbsY());
}
public PageBox getLastPage(CssContext c, Box box) {
return getPage(c, box.getAbsY() + box.getHeight() - 1);
}
public void ensureHasPage(CssContext c, Box box) {
getLastPage(c, box);
}
public PageBox getPage(CssContext c, int yOffset) {
List pages = getPages();
if (yOffset < 0) {
return null;
} else {
PageBox lastRequested = getLastRequestedPage();
if (lastRequested != null) {
if (yOffset >= lastRequested.getTop() && yOffset < lastRequested.getBottom()) {
return lastRequested;
}
}
PageBox last = pages.get(pages.size()-1);
if (yOffset < last.getBottom()) {
// The page we're looking for is probably at the end of the
// document so do a linear search for the first few pages
// and then fall back to a binary search if that doesn't work
// out
int count = pages.size();
for (int i = count-1; i >= 0 && i >= count-5; i--) {
PageBox pageBox = (PageBox)pages.get(i);
if (yOffset >= pageBox.getTop() && yOffset < pageBox.getBottom()) {
setLastRequestedPage(pageBox);
return pageBox;
}
}
int low = 0;
int high = count-6;
while (low <= high) {
int mid = (low + high) >> 1;
PageBox pageBox = (PageBox)pages.get(mid);
if (yOffset >= pageBox.getTop() && yOffset < pageBox.getBottom()) {
setLastRequestedPage(pageBox);
return pageBox;
}
if (pageBox.getTop() < yOffset) {
low = mid + 1;
} else {
high = mid - 1;
}
}
} else {
addPagesUntilPosition(c, yOffset);
PageBox result = (PageBox) pages.get(pages.size()-1);
setLastRequestedPage(result);
return result;
}
}
throw new RuntimeException("internal error");
}
private void addPagesUntilPosition(CssContext c, int position) {
List pages = getPages();
PageBox last = pages.get(pages.size()-1);
while (position >= last.getBottom()) {
addPage(c);
last = pages.get(pages.size()-1);
}
}
public void trimEmptyPages(CssContext c, int maxYHeight) {
// Empty pages may result when a "keep together" constraint
// cannot be satisfied and is dropped
List pages = getPages();
for (int i = pages.size() - 1; i > 0; i--) {
PageBox page = pages.get(i);
if (page.getTop() >= maxYHeight) {
if (page == getLastRequestedPage()) {
setLastRequestedPage(null);
}
pages.remove(i);
} else {
break;
}
}
}
public void trimPageCount(int newPageCount) {
while (_pages.size() > newPageCount) {
PageBox pageBox = _pages.remove(_pages.size()-1);
if (pageBox == getLastRequestedPage()) {
setLastRequestedPage(null);
}
}
}
public void assignPagePaintingPositions(CssContext cssCtx, short mode) {
assignPagePaintingPositions(cssCtx, mode, 0);
}
public void assignPagePaintingPositions(
CssContext cssCtx, int mode, int additionalClearance) {
List pages = getPages();
int paintingTop = additionalClearance;
for (PageBox page : pages) {
page.setPaintingTop(paintingTop);
if (mode == PAGED_MODE_SCREEN) {
page.setPaintingBottom(paintingTop + page.getHeight(cssCtx));
} else if (mode == PAGED_MODE_PRINT) {
page.setPaintingBottom(paintingTop + page.getContentHeight(cssCtx));
} else {
throw new IllegalArgumentException("Illegal mode");
}
paintingTop = page.getPaintingBottom() + additionalClearance;
}
}
public int getMaxPageWidth(CssContext cssCtx, int additionalClearance) {
List pages = getPages();
int maxWidth = 0;
for (PageBox page : pages) {
int pageWidth = page.getWidth(cssCtx) + additionalClearance * 2;
if (pageWidth > maxWidth) {
maxWidth = pageWidth;
}
}
return maxWidth;
}
public PageBox getLastPage() {
List pages = getPages();
return pages.size() == 0 ? null : pages.get(pages.size()-1);
}
public boolean crossesPageBreak(LayoutContext c, int top, int bottom) {
if (top < 0) {
return false;
}
PageBox page = getPage(c, top);
return bottom >= page.getBottom() - c.getExtraSpaceBottom();
}
public Layer findRoot() {
if (isRootLayer()) {
return this;
} else {
return getParent().findRoot();
}
}
public void addRunningBlock(BlockBox block) {
if (_runningBlocks == null) {
_runningBlocks = new HashMap<>();
}
String identifier = block.getStyle().getRunningName();
List blocks = _runningBlocks.get(identifier);
if (blocks == null) {
blocks = new ArrayList<>();
_runningBlocks.put(identifier, blocks);
}
blocks.add(block);
Collections.sort(blocks, (b1, b2) -> b1.getAbsY() - b2.getAbsY());
}
public void removeRunningBlock(BlockBox block) {
if (_runningBlocks == null) {
return;
}
String identifier = block.getStyle().getRunningName();
List blocks = _runningBlocks.get(identifier);
if (blocks == null) {
return;
}
blocks.remove(block);
}
public BlockBox getRunningBlock(String identifer, PageBox page, PageElementPosition which) {
if (_runningBlocks == null) {
return null;
}
List blocks = _runningBlocks.get(identifer);
if (blocks == null) {
return null;
}
if (which == PageElementPosition.START) {
BlockBox prev = null;
for (Iterator i = blocks.iterator(); i.hasNext(); ) {
BlockBox b = i.next();
if (b.getStaticEquivalent().getAbsY() >= page.getTop()) {
break;
}
prev = b;
}
return prev;
} else if (which == PageElementPosition.FIRST) {
for (Iterator i = blocks.iterator(); i.hasNext(); ) {
BlockBox b = i.next();
int absY = b.getStaticEquivalent().getAbsY();
if (absY >= page.getTop() && absY < page.getBottom()) {
return b;
}
}
return getRunningBlock(identifer, page, PageElementPosition.START);
} else if (which == PageElementPosition.LAST) {
BlockBox prev = null;
for (Iterator i = blocks.iterator(); i.hasNext(); ) {
BlockBox b = i.next();
if (b.getStaticEquivalent().getAbsY() > page.getBottom()) {
break;
}
prev = b;
}
return prev;
} else if (which == PageElementPosition.LAST_EXCEPT) {
BlockBox prev = null;
for (Iterator i = blocks.iterator(); i.hasNext(); ) {
BlockBox b = i.next();
int absY = b.getStaticEquivalent().getAbsY();
if (absY >= page.getTop() && absY < page.getBottom()) {
return null;
}
if (absY > page.getBottom()) {
break;
}
prev = b;
}
return prev;
}
throw new RuntimeException("bug: internal error");
}
public void layoutPages(LayoutContext c) {
c.setRootDocumentLayer(c.getRootLayer());
for (PageBox pageBox : _pages) {
pageBox.layout(c);
}
}
public void addPageSequence(BlockBox start) {
if (_pageSequences == null) {
_pageSequences = new HashSet<>();
}
_pageSequences.add(start);
}
private List getSortedPageSequences() {
if (_pageSequences == null) {
return null;
}
if (_sortedPageSequences == null) {
List result = new ArrayList<>(_pageSequences);
Collections.sort(result, new Comparator() {
public int compare(BlockBox b1, BlockBox b2) {
return b1.getAbsY() - b2.getAbsY();
}
});
_sortedPageSequences = result;
}
return _sortedPageSequences;
}
public int getRelativePageNo(RenderingContext c, int absY) {
List sequences = getSortedPageSequences();
int initial = 0;
if (c.getInitialPageNo() > 0) {
initial = c.getInitialPageNo() - 1;
}
if ((sequences == null) || sequences.isEmpty()) {
return initial + getPage(c, absY).getPageNo();
} else {
BlockBox pageSequence = findPageSequence(sequences, absY);
int sequenceStartAbsolutePageNo = getPage(c, pageSequence.getAbsY()).getPageNo();
int absoluteRequiredPageNo = getPage(c, absY).getPageNo();
return absoluteRequiredPageNo - sequenceStartAbsolutePageNo;
}
}
private BlockBox findPageSequence(List sequences, int absY) {
BlockBox result = null;
for (int i = 0; i < sequences.size(); i++) {
result = sequences.get(i);
if ((i < sequences.size() - 1) && ((sequences.get(i + 1)).getAbsY() > absY)) {
break;
}
}
return result;
}
public int getRelativePageNo(RenderingContext c) {
List sequences = getSortedPageSequences();
int initial = 0;
if (c.getInitialPageNo() > 0) {
initial = c.getInitialPageNo() - 1;
}
if (sequences == null) {
return initial + c.getPageNo();
} else {
int sequenceStartIndex = getPageSequenceStart(c, sequences, c.getPage());
if (sequenceStartIndex == -1) {
return initial + c.getPageNo();
} else {
BlockBox block = sequences.get(sequenceStartIndex);
return c.getPageNo() - getFirstPage(c, block).getPageNo();
}
}
}
public int getRelativePageCount(RenderingContext c) {
List sequences = getSortedPageSequences();
int initial = 0;
if (c.getInitialPageNo() > 0) {
initial = c.getInitialPageNo() - 1;
}
if (sequences == null) {
return initial + c.getPageCount();
} else {
int firstPage;
int lastPage;
int sequenceStartIndex = getPageSequenceStart(c, sequences, c.getPage());
if (sequenceStartIndex == -1) {
firstPage = 0;
} else {
BlockBox block = sequences.get(sequenceStartIndex);
firstPage = getFirstPage(c, block).getPageNo();
}
if (sequenceStartIndex < sequences.size() - 1) {
BlockBox block = sequences.get(sequenceStartIndex+1);
lastPage = getFirstPage(c, block).getPageNo();
} else {
lastPage = c.getPageCount();
}
int sequenceLength = lastPage - firstPage;
if (sequenceStartIndex == -1) {
sequenceLength += initial;
}
return sequenceLength;
}
}
private int getPageSequenceStart(RenderingContext c, List sequences, PageBox page) {
for (int i = sequences.size() - 1; i >= 0; i--) {
BlockBox start = sequences.get(i);
if (start.getAbsY() < page.getBottom() - 1) {
return i;
}
}
return -1;
}
public Box getSelectionEnd() {
return _selectionEnd;
}
public void setSelectionEnd(Box selectionEnd) {
_selectionEnd = selectionEnd;
}
public Box getSelectionStart() {
return _selectionStart;
}
public void setSelectionStart(Box selectionStart) {
_selectionStart = selectionStart;
}
public int getSelectionEndX() {
return _selectionEndX;
}
public void setSelectionEndX(int selectionEndX) {
_selectionEndX = selectionEndX;
}
public int getSelectionEndY() {
return _selectionEndY;
}
public void setSelectionEndY(int selectionEndY) {
_selectionEndY = selectionEndY;
}
public int getSelectionStartX() {
return _selectionStartX;
}
public void setSelectionStartX(int selectionStartX) {
_selectionStartX = selectionStartX;
}
public int getSelectionStartY() {
return _selectionStartY;
}
public void setSelectionStartY(int selectionStartY) {
_selectionStartY = selectionStartY;
}
private PageBox getLastRequestedPage() {
return _lastRequestedPage;
}
private void setLastRequestedPage(PageBox lastRequestedPage) {
_lastRequestedPage = lastRequestedPage;
}
}