All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.openhtmltopdf.render.FlowingColumnContainerBox Maven / Gradle / Ivy

Go to download

Open HTML to PDF is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code.

There is a newer version: 1.0.10
Show newest version
package com.openhtmltopdf.render;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.layout.BlockFormattingContext;
import com.openhtmltopdf.layout.FloatManager;
import com.openhtmltopdf.layout.FloatManager.BoxOffset;
import com.openhtmltopdf.layout.LayoutContext;
import com.openhtmltopdf.layout.PersistentBFC;

public class FlowingColumnContainerBox extends BlockBox {
    private FlowingColumnBox _child;

    // FIXME: Inefficient, replace with binary search.
    private int findPageIndex(List pages, int y) {
        int idx = 0;
        for (PageBox page : pages) {
            if (y >= page.getTop() && y <= page.getBottom()) {
                return idx;
            }
            idx++;
        }
        return idx - 1;
    }

    private static class ColumnPosition {
        private final int columnIndex;
        private final int copyY;  // Absolute, What y position starts the column in the long column block.
        private final int pasteY; // Absolute, What y position starts the column in the flowing column block for
                                  // final render.
        private final int maxColHeight; // Absolute, Maximum height of the column.
        private final int pageIdx;

        private ColumnPosition(int columnIndex, int copyY, int pasteY, int maxColHeight, int pageIdx) {
            this.columnIndex = columnIndex;
            this.copyY = copyY;
            this.pasteY = pasteY;
            this.maxColHeight = maxColHeight;
            this.pageIdx = pageIdx;
        }
        
        @Override
        public String toString() {
            return String.format("[index='%d', copyY='%d', pasteY='%d', maxColHeight='%d', pageIdx='%d']",
                           columnIndex, copyY, pasteY, maxColHeight, pageIdx);
        }
    }
    
    public static class ColumnBreakOpportunity {
        private final Box box;             // The box where we can break.
        private final List ancestors; // Ancestors of this box which should be moved with it.

        private ColumnBreakOpportunity(Box box, List ancestors) {
            this.box = box;
            this.ancestors = ancestors;
        }
        
        static ColumnBreakOpportunity of(Box box, List ancestors) {
            return new ColumnBreakOpportunity(box, ancestors);
        }
        
        @Override
        public String toString() {
            return String.valueOf(box);
        }
    }
    
    public static class ColumnBreakStore {
        // Break opportunity boxes.
        private final List breaks = new ArrayList<>();
        // Which container boxes have been processed, so we don't move them twice.
        private final Set processedContainers = new HashSet<>();
        
        /**
         * Add a break opportunity. If this is a break opportunity and a first child, it 
         * should also add all unprocessed ancestors, so they can be moved with the
         * first child.
         */
        public void addBreak(Box box, List ancestors) {
            breaks.add(ColumnBreakOpportunity.of(box, ancestors));
        }
        
        /**
         * Whether an ancestor box needs to be added to the list of ancestors.
         * @return true to process this ancestor (we haven't seen it yet).
         */
        public boolean checkContainerShouldProcess(Box container) {
            if (container instanceof FlowingColumnContainerBox ||
                container instanceof FlowingColumnBox) {
                return false;
            }
            
            return processedContainers.add(container);
        }

        @Override
        public String toString() {
            return breaks.toString();
        }
    }
    
    private void layoutFloats(TreeMap columns, List floats, int columnCount, int colWidth, int colGap) {
        for (BoxOffset bo : floats) {
            BlockBox floater = bo.getBox();
            
            ColumnBreakStore store = new ColumnBreakStore();
            floater.findColumnBreakOpportunities(store);
            
            for (ColumnBreakOpportunity breakOp : store.breaks) {
                Map.Entry entry = columns.floorEntry(breakOp.box.getAbsY());
                ColumnPosition column = entry.getValue();
            
                int yAdjust = column.pasteY - column.copyY;
                int xAdjust = ((column.columnIndex % columnCount) * colWidth) + ((column.columnIndex % columnCount) * colGap);

                reposition(breakOp.box, xAdjust, yAdjust);
                
                if (breakOp.ancestors != null) {
                    repositionAncestors(breakOp.ancestors, xAdjust, yAdjust);
                }
                
                if (breakOp.box instanceof LineBox) {
                    breakOp.box.calcChildLocations();
                }
            }
        }
    }

    private void layoutFloats(TreeMap columnMap, PersistentBFC bfc, int columnCount, int colWidth, int colGap) {
        List floatsL = this.getPersistentBFC().getFloatManager().getFloats(FloatManager.FloatDirection.LEFT);
        List floatsR = this.getPersistentBFC().getFloatManager().getFloats(FloatManager.FloatDirection.RIGHT);

        layoutFloats(columnMap, floatsL, columnCount, colWidth, colGap);
        layoutFloats(columnMap, floatsR, columnCount, colWidth, colGap);
    }
    
    private void reposition(Box box, int xAdjust, int yAdjust) {
        if (box instanceof BlockBox &&
            ((BlockBox) box).isFloated()) {
            box.setX(box.getX() + xAdjust);
            box.setY(box.getY() + yAdjust);
        } else {
            box.setAbsY(box.getAbsY() + yAdjust);
            box.setAbsX(box.getAbsX() + xAdjust);
        }
    }
    
    private void repositionAncestors(List ancestors, int xAdjust, int yAdjust) {
        for (Box ancestor : ancestors) {
            reposition(ancestor, xAdjust, yAdjust);
        }

        // FIXME: We do not resize or duplicate ancestor container boxes,
        // so if user has used border, background color
        // or overflow: hidden it will produce incorrect results.
    }
    
    private int adjustUnbalanced(LayoutContext c, Box child, int colGap, int colWidth, int columnCount, int xStart) {
        // At the start of this method we have one long column in child.
        // This method works by going through the boxes and adjusting their position
        // into the current column.
        
        final int startY = this.getAbsY();
        final List pages = c.getRootLayer().getPages();
        
        final boolean haveFloats = 
                !this.getPersistentBFC().getFloatManager().getFloats(FloatManager.FloatDirection.LEFT).isEmpty() ||
                !this.getPersistentBFC().getFloatManager().getFloats(FloatManager.FloatDirection.RIGHT).isEmpty();
        
        // We only need the tree map if we have floats.
        final TreeMap columnMap = haveFloats ? new TreeMap<>() : null;
        
        // These are all running values that change as we layout our boxes into columns.
        int pageIdx      = findPageIndex(pages, startY);
        int colStart     = startY;
        int colHeight    = pages.get(pageIdx).getBottom() - this.getChild().getAbsY();
        int colIdx       = 0;
        int finalHeight  = 0;

        if (child.getHeight() <= colHeight) {
            // We fit in the first column.
            return child.getHeight();
        }
        
        // Recursively find all the column break opportunities (typically line boxes).
        ColumnBreakStore store = new ColumnBreakStore();
        child.findColumnBreakOpportunities(store);
        
        if (store.breaks.isEmpty() || store.breaks.size() == 1) {
            // Nothing we can do except overflow.
            // The only break is at the start of the first child.
            return this.getChild().getHeight();
        }

        // Add our first column.
        ColumnPosition current = new ColumnPosition(colIdx, /* copy-from */ colStart, /* copy-to */ colStart, colHeight, pageIdx);
        if (haveFloats) {
            columnMap.put(colStart, current);
        }
         
        // FIXME: Don't sort if we have in order - common case.
        Collections.sort(store.breaks, 
                Comparator.comparingInt(brk -> brk.box.getAbsY() + brk.box.getBorderBoxHeight(c)));
        
        for (int i = 0; i < store.breaks.size(); i++) {
            ColumnBreakOpportunity br = store.breaks.get(i);
            ColumnBreakOpportunity nextBr = i < store.breaks.size() - 1 ? store.breaks.get(i + 1) : null;
            Box ch = br.box;

            int yAdjust = current.pasteY - current.copyY;
            int yProposedFinal = ch.getAbsY() + yAdjust;
            ch.setAbsY(yProposedFinal);

            // We need the max height of the column which is the bottom of the current box
            // minus the top of the column.
            finalHeight = Math.max((yProposedFinal + ch.getBorderBoxHeight(c)) - startY, finalHeight);

            // x position should be easy.
            int xAdjust = ((colIdx % columnCount) * colWidth) + ((colIdx % columnCount) * colGap);
            ch.setAbsX(ch.getAbsX() + xAdjust);

            if (br.ancestors != null) {
                // We move container ancestors with the first child that is
                // a break opportunity.
                // EXAMPLE: column box -> p -> ul -> li -> line box
                // We would move the p, ul and li on the first line of the first li.
                // For the second li we only have to move the parent li as p and ul have
                // already been processed.
                repositionAncestors(br.ancestors, xAdjust, yAdjust);
            }
            
            if (ch instanceof LineBox) {
                // We do not call this on other kind of boxes as it would undo our work in moving them.
                ch.calcChildLocations();
            }
 
            if (nextBr != null) {
                Box next = nextBr.box;
                int nextYHeight = next.getAbsY() + yAdjust + next.getBorderBoxHeight(c) - current.pasteY;
                
                if (nextYHeight > current.maxColHeight ||
                    ch.getStyle().isColumnBreakAfter() ||
                    next.getStyle().isColumnBreakBefore()) {
                    // We have moved past the bottom of the current column (or explicit break).
                    // Time for a new column.
                    // FIXME: What if box doesn't fit in new column either?
                    int newColIdx = colIdx + 1;
                
                    // And possibly a new page.
                    boolean needNewPage = newColIdx % columnCount == 0;
                    int newPageIdx = needNewPage ? current.pageIdx + 1 : current.pageIdx;

                    if (newPageIdx >= pages.size()) {
                        c.getRootLayer().addPage(c);
                    }

                    // We need the y top of the new column.
                    PageBox page = pages.get(newPageIdx);
                    int pasteY = needNewPage ? page.getTop() : current.pasteY;
                    int copyY  = next.getAbsY();
                    
                    current = new ColumnPosition(newColIdx, copyY, pasteY, page.getBottom() - pasteY, newPageIdx);
                    if (haveFloats) {
                        columnMap.put(copyY, current);
                    }
                    colIdx++;
                }
            }
        }
        
        if (haveFloats) {
            layoutFloats(columnMap, this.getPersistentBFC(), columnCount, colWidth, colGap);
        }

        return finalHeight;
    }

    @Override
    public void layout(LayoutContext c, int contentStart) {
        BlockFormattingContext bfc = new BlockFormattingContext(this, c);
        c.pushBFC(bfc);
        
        addBoxID(c);
        
        this.calcDimensions(c);

        int colCount = getStyle().columnCount();
        int colGapCount = colCount - 1;

        float colGap = getStyle().isIdent(CSSName.COLUMN_GAP, IdentValue.NORMAL) ? getStyle().getLineHeight(c)
                : /* Use the line height as a normal column gap. */
                getStyle().getFloatPropertyProportionalWidth(CSSName.COLUMN_GAP, getContentWidth(), c);

        float totalGap = colGap * colGapCount;
        int colWidth = (int) ((this.getContentWidth() - totalGap) / colCount);

        _child.setContainingLayer(this.getContainingLayer());
        _child.setContentWidth(colWidth);
        _child.setColumnWidth(colWidth);
        _child.setAbsX(this.getAbsX());
        _child.setAbsY(this.getAbsY());

        c.setIsPrintOverride(false);
        _child.layout(c, contentStart);
        c.setIsPrintOverride(null);

        int height = adjustUnbalanced(c, _child, (int) colGap, colWidth, colCount, this.getLeftMBP() + this.getX());
        _child.setHeight(0);
        this.setHeight(height);
        c.popBFC();
    }

    public void setOnlyChild(LayoutContext c, FlowingColumnBox child) {
        this._child = child;
        this.addChild(child);
    }

    public FlowingColumnBox getChild() {
        return _child;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy