/* * 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.LinkedList; import java.util.List; import java.util.ListIterator; import org.apache.fop.area.Block; 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.PageSequence; 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; /** * The FlowLayoutManager object, which processes * the single fo:flow of the fo:page-sequence */ private FlowLayoutManager childFLM = null; private StaticContentLayoutManager footnoteSeparatorLM = null; 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(); } /** {@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); } if (needColumnBalancing) { AbstractBreaker.log.debug( "Column balancing necessary for the next element list!!!"); } return nextSequenceStartsOn; } /** {@inheritDoc} */ protected int getNextBlockList(LayoutContext childLC, int nextSequenceStartsOn) { 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()); return super.getNextBlockList(childLC, nextSequenceStartsOn); } /** {@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 boolean bFootnotesPresent = 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 bFootnotesPresent = 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)); } } } } if (bFootnotesPresent) { // handle the footnote separator 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 = (StaticContentLayoutManager) pslm.getLayoutManagerMaker().makeStaticContentLayoutManager( pslm, footnoteSeparator, separatorArea); footnoteSeparatorLM.doLayout(); footnoteSeparatorLength = new MinOptMax(separatorArea.getBPD()); } } 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 = (StaticContentLayoutManager) pslm.getLayoutManagerMaker().makeStaticContentLayoutManager( pslm, footnoteSeparator, separatorArea); footnoteSeparatorLM.doLayout(); } childFLM.addAreas(posIter, context); } /** * Performs phase 3 operation * * @param alg page breaking algorithm * @param partCount part count * @param originalList the block sequence original list * @param effectiveList the block sequence effective list */ protected void doPhase3(PageBreakingAlgorithm alg, int partCount, BlockSequence originalList, BlockSequence effectiveList) { if (needColumnBalancing) { doPhase3WithColumnBalancing(alg, partCount, originalList, effectiveList); } else { if (!hasMoreContent() && pslm.getPageSequence().hasPagePositionLast()) { //last part is reached and we have a "last page" condition doPhase3WithLastPage(alg, partCount, originalList, effectiveList); } else { //Directly add areas after finding the breaks addAreas(alg, partCount, originalList, effectiveList); } } } private void doPhase3WithLastPage(PageBreakingAlgorithm alg, int partCount, BlockSequence originalList, BlockSequence effectiveList) { int newStartPos; int restartPoint = pageProvider.getStartingPartIndexForLastPage(partCount); if (restartPoint > 0) { //Add definitive areas before last page addAreas(alg, restartPoint, originalList, effectiveList); //Get page break from which we restart PageBreakPosition pbp = (PageBreakPosition) alg.getPageBreaks().get(restartPoint - 1); newStartPos = pbp.getLeafPos(); //Handle page break right here to avoid any side-effects if (newStartPos > 0) { handleBreakTrait(Constants.EN_PAGE); } } else { newStartPos = 0; } AbstractBreaker.log.debug("Last page handling now!!!"); AbstractBreaker.log.debug("==================================================="); 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()); pageProvider.setLastPageIndex(currentPageNum); //Restart last page PageBreakingAlgorithm algRestart = new PageBreakingAlgorithm( getTopLevelLM(), getPageProvider(), createLayoutListener(), alg.getAlignment(), alg.getAlignmentLast(), footnoteSeparatorLength, isPartOverflowRecoveryActivated(), false, false); //alg.setConstantLineWidth(flowBPD); int iOptPageCount = algRestart.findBreakingPoints(effectiveList, newStartPos, 1, true, BreakingAlgorithm.ALL_BREAKS); AbstractBreaker.log.debug("restart: iOptPageCount= " + iOptPageCount + " pageBreaks.size()= " + algRestart.getPageBreaks().size()); boolean replaceLastPage = iOptPageCount <= pslm.getCurrentPV().getBodyRegion().getColumnCount(); if (replaceLastPage) { //Replace last page pslm.setCurrentPage(pageProvider.getPage(false, currentPageNum)); //Make sure we only add the areas we haven't added already effectiveList.ignoreAtStart = newStartPos; addAreas(algRestart, iOptPageCount, originalList, effectiveList); } else { effectiveList.ignoreAtStart = newStartPos; addAreas(alg, restartPoint, partCount - restartPoint, originalList, effectiveList); //Add blank last page pageProvider.setLastPageIndex(currentPageNum + 1); pslm.setCurrentPage(pslm.makeNewPage(true, true)); } AbstractBreaker.log.debug("==================================================="); } private void doPhase3WithColumnBalancing(PageBreakingAlgorithm alg, int partCount, BlockSequence originalList, BlockSequence effectiveList) { AbstractBreaker.log.debug("Column balancing now!!!"); AbstractBreaker.log.debug("==================================================="); int newStartPos; int restartPoint = pageProvider.getStartingPartIndexForLastPage(partCount); if (restartPoint > 0) { //Add definitive areas addAreas(alg, restartPoint, originalList, effectiveList); //Get page break from which we restart PageBreakPosition pbp = (PageBreakPosition) alg.getPageBreaks().get(restartPoint - 1); newStartPos = pbp.getLeafPos(); //Handle page break right here to avoid any side-effects if (newStartPos > 0) { handleBreakTrait(Constants.EN_PAGE); } } else { newStartPos = 0; } AbstractBreaker.log.debug("Restarting at " + restartPoint + ", new start position: " + newStartPos); pageBreakHandled = true; //Update so the available BPD is reported correctly pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(), pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex()); //Restart last page PageBreakingAlgorithm algRestart = new BalancingColumnBreakingAlgorithm( getTopLevelLM(), getPageProvider(), createLayoutListener(), alignment, Constants.EN_START, footnoteSeparatorLength, isPartOverflowRecoveryActivated(), pslm.getCurrentPV().getBodyRegion().getColumnCount()); //alg.setConstantLineWidth(flowBPD); int iOptPageCount = algRestart.findBreakingPoints(effectiveList, newStartPos, 1, true, BreakingAlgorithm.ALL_BREAKS); AbstractBreaker.log.debug("restart: iOptPageCount= " + iOptPageCount + " pageBreaks.size()= " + algRestart.getPageBreaks().size()); if (iOptPageCount > pslm.getCurrentPV().getBodyRegion().getColumnCount()) { 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."); */ } //Make sure we only add the areas we haven't added already effectiveList.ignoreAtStart = newStartPos; addAreas(algRestart, iOptPageCount, originalList, effectiveList); AbstractBreaker.log.debug("==================================================="); } protected void startPart(BlockSequence list, int breakClass) { AbstractBreaker.log.debug("startPart() breakClass=" + 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()); } 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(); } 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++) { LinkedList 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 = (Footnote) pslm.getCurrentPV().getBodyRegion().getFootnote(); int topOffset = (int) pslm.getCurrentPV().getBodyRegion().getBPD() - parentArea.getBPD(); if (separatorArea != null) { topOffset -= separatorArea.getBPD(); } parentArea.setTop(topOffset); parentArea.setSeparator(separatorArea); } pslm.getCurrentPV().getCurrentSpan().notifyFlowsFinished(); } /** * @return the current child flow layout manager */ protected LayoutManager getCurrentChildLM() { return childFLM; } /** {@inheritDoc} */ protected void observeElementList(List elementList) { ElementListObserver.observe(elementList, "breaker", ((PageSequence)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(); if (breakVal == Constants.EN_ALL) { //break due to span change in multi-column layout curPage.getPageViewport().createSpan(true); return; } else if (breakVal == Constants.EN_NONE) { curPage.getPageViewport().createSpan(false); return; } else if (breakVal == Constants.EN_COLUMN || breakVal <= 0 || breakVal == Constants.EN_AUTO) { PageViewport pv = curPage.getPageViewport(); //Check if previous page was spanned boolean forceNewPageWithSpan = false; RegionBody rb = (RegionBody)curPage.getSimplePageMaster().getRegion( Constants.FO_REGION_BODY); if (breakVal < 0 && rb.getColumnCount() > 1 && pv.getCurrentSpan().getColumnCount() == 1) { forceNewPageWithSpan = true; } if (forceNewPageWithSpan) { curPage = pslm.makeNewPage(false, false); curPage.getPageViewport().createSpan(true); } else if (pv.getCurrentSpan().hasMoreFlows()) { pv.getCurrentSpan().moveToNextFlow(); } else { curPage = pslm.makeNewPage(false, false); } return; } log.debug("handling break-before after page " + pslm.getCurrentPageNum() + " breakVal=" + breakVal); if (needBlankPageBeforeNew(breakVal)) { curPage = pslm.makeNewPage(true, false); } if (needNewPage(breakVal)) { 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; } } }