/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* $Id$ */ package org.apache.fop.layoutmgr; import java.util.List; import java.util.ListIterator; import org.apache.fop.area.Block; import org.apache.fop.area.BodyRegion; import org.apache.fop.area.Footnote; import org.apache.fop.area.PageViewport; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FObj; import org.apache.fop.fo.pagination.Region; import org.apache.fop.fo.pagination.RegionBody; import org.apache.fop.fo.pagination.StaticContent; import org.apache.fop.layoutmgr.PageBreakingAlgorithm.PageBreakingLayoutListener; import org.apache.fop.traits.MinOptMax; /** * Handles the breaking of pages in an fo:flow */ public class PageBreaker extends AbstractBreaker { private PageSequenceLayoutManager pslm; private boolean firstPart = true; private boolean pageBreakHandled; private boolean needColumnBalancing; private PageProvider pageProvider; private Block separatorArea; private boolean spanAllActive; /** * The FlowLayoutManager object, which processes * the single fo:flow of the fo:page-sequence */ private FlowLayoutManager childFLM = null; private StaticContentLayoutManager footnoteSeparatorLM = null; /** * Construct page breaker. * @param pslm the page sequence layout manager */ public PageBreaker(PageSequenceLayoutManager pslm) { this.pslm = pslm; this.pageProvider = pslm.getPageProvider(); this.childFLM = pslm.getLayoutManagerMaker().makeFlowLayoutManager( pslm, pslm.getPageSequence().getMainFlow()); } /** {@inheritDoc} */ protected void updateLayoutContext(LayoutContext context) { int flowIPD = pslm.getCurrentPV().getCurrentSpan().getColumnWidth(); context.setRefIPD(flowIPD); } /** {@inheritDoc} */ protected LayoutManager getTopLevelLM() { return pslm; } /** {@inheritDoc} */ protected PageProvider getPageProvider() { return pslm.getPageProvider(); } /** * Starts the page breaking process. * @param flowBPD the constant available block-progression-dimension (used for every part) */ void doLayout(int flowBPD) { doLayout(flowBPD, false); } /** {@inheritDoc} */ protected PageBreakingLayoutListener createLayoutListener() { return new PageBreakingLayoutListener() { public void notifyOverflow(int part, int amount, FObj obj) { Page p = pageProvider.getPage( false, part, PageProvider.RELTO_CURRENT_ELEMENT_LIST); RegionBody body = (RegionBody)p.getSimplePageMaster().getRegion( Region.FO_REGION_BODY); BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get( body.getUserAgent().getEventBroadcaster()); boolean canRecover = (body.getOverflow() != Constants.EN_ERROR_IF_OVERFLOW); boolean needClip = (body.getOverflow() == Constants.EN_HIDDEN || body.getOverflow() == Constants.EN_ERROR_IF_OVERFLOW); eventProducer.regionOverflow(this, body.getName(), p.getPageViewport().getPageNumberString(), amount, needClip, canRecover, body.getLocator()); } }; } /** {@inheritDoc} */ protected int handleSpanChange(LayoutContext childLC, int nextSequenceStartsOn) { needColumnBalancing = false; if (childLC.getNextSpan() != Constants.NOT_SET) { //Next block list will have a different span. nextSequenceStartsOn = childLC.getNextSpan(); needColumnBalancing = childLC.getNextSpan() == Constants.EN_ALL && childLC.getDisableColumnBalancing() == Constants.EN_FALSE; } if (needColumnBalancing) { AbstractBreaker.log.debug( "Column balancing necessary for the next element list!!!"); } return nextSequenceStartsOn; } /** {@inheritDoc} */ protected int getNextBlockList(LayoutContext childLC, int nextSequenceStartsOn) { return getNextBlockList(childLC, nextSequenceStartsOn, null, null, null); } /** {@inheritDoc} */ protected int getNextBlockList(LayoutContext childLC, int nextSequenceStartsOn, Position positionAtIPDChange, LayoutManager restartLM, List firstElements) { if (!firstPart) { // if this is the first page that will be created by // the current BlockSequence, it could have a break // condition that must be satisfied; // otherwise, we may simply need a new page handleBreakTrait(nextSequenceStartsOn); } firstPart = false; pageBreakHandled = true; pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(), pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(), this.spanAllActive); return super.getNextBlockList(childLC, nextSequenceStartsOn, positionAtIPDChange, restartLM, firstElements); } private boolean containsFootnotes(List contentList, LayoutContext context) { boolean containsFootnotes = false; if (contentList != null) { ListIterator contentListIterator = contentList.listIterator(); while (contentListIterator.hasNext()) { ListElement element = (ListElement) contentListIterator.next(); if (element instanceof KnuthBlockBox && ((KnuthBlockBox) element).hasAnchors()) { // element represents a line with footnote citations containsFootnotes = true; LayoutContext footnoteContext = new LayoutContext(context); footnoteContext.setStackLimitBP(context.getStackLimitBP()); footnoteContext.setRefIPD(pslm.getCurrentPV() .getRegionReference(Constants.FO_REGION_BODY).getIPD()); List footnoteBodyLMs = ((KnuthBlockBox) element).getFootnoteBodyLMs(); ListIterator footnoteBodyIterator = footnoteBodyLMs.listIterator(); // store the lists of elements representing the footnote bodies // in the box representing the line containing their references while (footnoteBodyIterator.hasNext()) { FootnoteBodyLayoutManager fblm = (FootnoteBodyLayoutManager) footnoteBodyIterator.next(); fblm.setParent(childFLM); fblm.initialize(); ((KnuthBlockBox) element).addElementList( fblm.getNextKnuthElements(footnoteContext, alignment)); } } } } return containsFootnotes; } private void handleFootnoteSeparator() { StaticContent footnoteSeparator; footnoteSeparator = pslm.getPageSequence().getStaticContent("xsl-footnote-separator"); if (footnoteSeparator != null) { // the footnote separator can contain page-dependent content such as // page numbers or retrieve markers, so its areas cannot simply be // obtained now and repeated in each page; // we need to know in advance the separator bpd: the actual separator // could be different from page to page, but its bpd would likely be // always the same // create a Block area that will contain the separator areas separatorArea = new Block(); separatorArea.setIPD(pslm.getCurrentPV() .getRegionReference(Constants.FO_REGION_BODY).getIPD()); // create a StaticContentLM for the footnote separator footnoteSeparatorLM = pslm.getLayoutManagerMaker().makeStaticContentLayoutManager( pslm, footnoteSeparator, separatorArea); footnoteSeparatorLM.doLayout(); footnoteSeparatorLength = MinOptMax.getInstance(separatorArea.getBPD()); } } /** {@inheritDoc} */ protected List getNextKnuthElements(LayoutContext context, int alignment) { List contentList = null; while (!childFLM.isFinished() && contentList == null) { contentList = childFLM.getNextKnuthElements(context, alignment); } // scan contentList, searching for footnotes if (containsFootnotes(contentList, context)) { // handle the footnote separator handleFootnoteSeparator(); } return contentList; } /** {@inheritDoc} */ protected List getNextKnuthElements(LayoutContext context, int alignment, Position positionAtIPDChange, LayoutManager restartAtLM) { List contentList = null; do { contentList = childFLM.getNextKnuthElements(context, alignment, positionAtIPDChange, restartAtLM); } while (!childFLM.isFinished() && contentList == null); // scan contentList, searching for footnotes if (containsFootnotes(contentList, context)) { // handle the footnote separator handleFootnoteSeparator(); } return contentList; } /** * @return current display alignment */ protected int getCurrentDisplayAlign() { return pslm.getCurrentPage().getSimplePageMaster().getRegion( Constants.FO_REGION_BODY).getDisplayAlign(); } /** * @return whether or not this flow has more page break opportunities */ protected boolean hasMoreContent() { return !childFLM.isFinished(); } /** * Adds an area to the flow layout manager * @param posIter the position iterator * @param context the layout context */ protected void addAreas(PositionIterator posIter, LayoutContext context) { if (footnoteSeparatorLM != null) { StaticContent footnoteSeparator = pslm.getPageSequence().getStaticContent( "xsl-footnote-separator"); // create a Block area that will contain the separator areas separatorArea = new Block(); separatorArea.setIPD( pslm.getCurrentPV().getRegionReference(Constants.FO_REGION_BODY).getIPD()); // create a StaticContentLM for the footnote separator footnoteSeparatorLM = pslm.getLayoutManagerMaker().makeStaticContentLayoutManager( pslm, footnoteSeparator, separatorArea); footnoteSeparatorLM.doLayout(); } childFLM.addAreas(posIter, context); } /** * {@inheritDoc} * This implementation checks whether to trigger column-balancing, * or whether to take into account a 'last-page' condition. */ protected void doPhase3(PageBreakingAlgorithm alg, int partCount, BlockSequence originalList, BlockSequence effectiveList) { if (needColumnBalancing) { //column balancing for the last part redoLayout(alg, partCount, originalList, effectiveList); return; } boolean lastPageMasterDefined = pslm.getPageSequence().hasPagePositionLast(); if (!hasMoreContent()) { //last part is reached if (lastPageMasterDefined) { //last-page condition redoLayout(alg, partCount, originalList, effectiveList); return; } } //nothing special: just add the areas now addAreas(alg, partCount, originalList, effectiveList); } /** * Restart the algorithm at the break corresponding to the given partCount. Used to * re-do the part after the last break in case of either column-balancing or a last * page-master. */ private void redoLayout(PageBreakingAlgorithm alg, int partCount, BlockSequence originalList, BlockSequence effectiveList) { int newStartPos = 0; int restartPoint = pageProvider.getStartingPartIndexForLastPage(partCount); if (restartPoint > 0) { //Add definitive areas for the parts before the //restarting point addAreas(alg, restartPoint, originalList, effectiveList); //Get page break from which we restart PageBreakPosition pbp = (PageBreakPosition) alg.getPageBreaks().get(restartPoint - 1); newStartPos = pbp.getLeafPos() + 1; //Handle page break right here to avoid any side-effects if (newStartPos > 0) { handleBreakTrait(Constants.EN_PAGE); } } AbstractBreaker.log.debug("Restarting at " + restartPoint + ", new start position: " + newStartPos); pageBreakHandled = true; //Update so the available BPD is reported correctly int currentPageNum = pslm.getCurrentPageNum(); pageProvider.setStartOfNextElementList(currentPageNum, pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(), this.spanAllActive); //Make sure we only add the areas we haven't added already effectiveList.ignoreAtStart = newStartPos; PageBreakingAlgorithm algRestart; if (needColumnBalancing) { AbstractBreaker.log.debug("Column balancing now!!!"); AbstractBreaker.log.debug("==================================================="); //Restart last page algRestart = new BalancingColumnBreakingAlgorithm( getTopLevelLM(), getPageProvider(), createLayoutListener(), alignment, Constants.EN_START, footnoteSeparatorLength, isPartOverflowRecoveryActivated(), pslm.getCurrentPV().getBodyRegion().getColumnCount()); AbstractBreaker.log.debug("==================================================="); } else { // Handle special page-master for last page BodyRegion currentBody = pageProvider.getPage(false, currentPageNum) .getPageViewport().getBodyRegion(); setLastPageIndex(currentPageNum); BodyRegion lastBody = pageProvider.getPage(false, currentPageNum) .getPageViewport().getBodyRegion(); lastBody.getMainReference().setSpans(currentBody.getMainReference().getSpans()); AbstractBreaker.log.debug("Last page handling now!!!"); AbstractBreaker.log.debug("==================================================="); //Restart last page algRestart = new PageBreakingAlgorithm( getTopLevelLM(), getPageProvider(), createLayoutListener(), alg.getAlignment(), alg.getAlignmentLast(), footnoteSeparatorLength, isPartOverflowRecoveryActivated(), false, false); AbstractBreaker.log.debug("==================================================="); } int optimalPageCount = algRestart.findBreakingPoints(effectiveList, newStartPos, 1, true, BreakingAlgorithm.ALL_BREAKS); AbstractBreaker.log.debug("restart: optimalPageCount= " + optimalPageCount + " pageBreaks.size()= " + algRestart.getPageBreaks().size()); boolean fitsOnePage = optimalPageCount <= pslm.getCurrentPV() .getBodyRegion().getMainReference().getCurrentSpan().getColumnCount(); if (needColumnBalancing) { if (!fitsOnePage) { AbstractBreaker.log.warn( "Breaking algorithm produced more columns than are available."); /* reenable when everything works throw new IllegalStateException( "Breaking algorithm must not produce more columns than available."); */ } } else { if (fitsOnePage) { //Replace last page pslm.setCurrentPage(pageProvider.getPage(false, currentPageNum)); } else { //Last page-master cannot hold the content. //Add areas now... addAreas(alg, restartPoint, partCount - restartPoint, originalList, effectiveList); //...and add a blank last page setLastPageIndex(currentPageNum + 1); pslm.setCurrentPage(pslm.makeNewPage(true, true)); return; } } addAreas(algRestart, optimalPageCount, originalList, effectiveList); } private void setLastPageIndex(int currentPageNum) { int lastPageIndex = pslm.getForcedLastPageNum(currentPageNum); pageProvider.setLastPageIndex(lastPageIndex); } /** {@inheritDoc} */ protected void startPart(BlockSequence list, int breakClass) { AbstractBreaker.log.debug("startPart() breakClass=" + getBreakClassName(breakClass)); if (pslm.getCurrentPage() == null) { throw new IllegalStateException("curPage must not be null"); } if (!pageBreakHandled) { //firstPart is necessary because we need the first page before we start the //algorithm so we have a BPD and IPD. This may subject to change later when we //start handling more complex cases. if (!firstPart) { // if this is the first page that will be created by // the current BlockSequence, it could have a break // condition that must be satisfied; // otherwise, we may simply need a new page handleBreakTrait(breakClass); } pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(), pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(), this.spanAllActive); } pageBreakHandled = false; // add static areas and resolve any new id areas // finish page and add to area tree firstPart = false; } /** {@inheritDoc} */ protected void handleEmptyContent() { pslm.getCurrentPV().getPage().fakeNonEmpty(); } /** {@inheritDoc} */ protected void finishPart(PageBreakingAlgorithm alg, PageBreakPosition pbp) { // add footnote areas if (pbp.footnoteFirstListIndex < pbp.footnoteLastListIndex || pbp.footnoteFirstElementIndex <= pbp.footnoteLastElementIndex) { // call addAreas() for each FootnoteBodyLM for (int i = pbp.footnoteFirstListIndex; i <= pbp.footnoteLastListIndex; i++) { List elementList = alg.getFootnoteList(i); int firstIndex = (i == pbp.footnoteFirstListIndex ? pbp.footnoteFirstElementIndex : 0); int lastIndex = (i == pbp.footnoteLastListIndex ? pbp.footnoteLastElementIndex : elementList.size() - 1); SpaceResolver.performConditionalsNotification(elementList, firstIndex, lastIndex, -1); LayoutContext childLC = new LayoutContext(0); AreaAdditionUtil.addAreas(null, new KnuthPossPosIter(elementList, firstIndex, lastIndex + 1), childLC); } // set the offset from the top margin Footnote parentArea = pslm.getCurrentPV().getBodyRegion().getFootnote(); int topOffset = pslm.getCurrentPV().getBodyRegion().getBPD() - parentArea.getBPD(); if (separatorArea != null) { topOffset -= separatorArea.getBPD(); } parentArea.setTop(topOffset); parentArea.setSeparator(separatorArea); } pslm.getCurrentPV().getCurrentSpan().notifyFlowsFinished(); } /** {@inheritDoc} */ protected LayoutManager getCurrentChildLM() { return childFLM; } /** {@inheritDoc} */ protected void observeElementList(List elementList) { ElementListObserver.observe(elementList, "breaker", pslm.getFObj().getId()); } /** * Depending on the kind of break condition, move to next column * or page. May need to make an empty page if next page would * not have the desired "handedness". * @param breakVal - value of break-before or break-after trait. */ private void handleBreakTrait(int breakVal) { Page curPage = pslm.getCurrentPage(); switch (breakVal) { case Constants.EN_ALL: //break due to span change in multi-column layout curPage.getPageViewport().createSpan(true); this.spanAllActive = true; return; case Constants.EN_NONE: curPage.getPageViewport().createSpan(false); this.spanAllActive = false; return; case Constants.EN_COLUMN: case Constants.EN_AUTO: case Constants.EN_PAGE: case -1: PageViewport pv = curPage.getPageViewport(); //Check if previous page was spanned boolean forceNewPageWithSpan = false; RegionBody rb = (RegionBody)curPage.getSimplePageMaster().getRegion( Constants.FO_REGION_BODY); forceNewPageWithSpan = (rb.getColumnCount() > 1 && pv.getCurrentSpan().getColumnCount() == 1); if (forceNewPageWithSpan) { log.trace("Forcing new page with span"); curPage = pslm.makeNewPage(false, false); curPage.getPageViewport().createSpan(true); } else if (pv.getCurrentSpan().hasMoreFlows()) { log.trace("Moving to next flow"); pv.getCurrentSpan().moveToNextFlow(); } else { log.trace("Making new page"); /*curPage = */pslm.makeNewPage(false, false); } return; default: log.debug("handling break-before after page " + pslm.getCurrentPageNum() + " breakVal=" + getBreakClassName(breakVal)); if (needBlankPageBeforeNew(breakVal)) { log.trace("Inserting blank page"); /*curPage = */pslm.makeNewPage(true, false); } if (needNewPage(breakVal)) { log.trace("Making new page"); /*curPage = */pslm.makeNewPage(false, false); } } } /** * Check if a blank page is needed to accomodate * desired even or odd page number. * @param breakVal - value of break-before or break-after trait. */ private boolean needBlankPageBeforeNew(int breakVal) { if (breakVal == Constants.EN_PAGE || (pslm.getCurrentPage().getPageViewport().getPage().isEmpty())) { // any page is OK or we already have an empty page return false; } else { /* IF we are on the kind of page we need, we'll need a new page. */ if (pslm.getCurrentPageNum() % 2 == 0) { // even page return (breakVal == Constants.EN_EVEN_PAGE); } else { // odd page return (breakVal == Constants.EN_ODD_PAGE); } } } /** * See if need to generate a new page * @param breakVal - value of break-before or break-after trait. */ private boolean needNewPage(int breakVal) { if (pslm.getCurrentPage().getPageViewport().getPage().isEmpty()) { if (breakVal == Constants.EN_PAGE) { return false; } else if (pslm.getCurrentPageNum() % 2 == 0) { // even page return (breakVal == Constants.EN_ODD_PAGE); } else { // odd page return (breakVal == Constants.EN_EVEN_PAGE); } } else { return true; } } }