| /* |
| * 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 org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.area.AreaTreeHandler; |
| import org.apache.fop.area.BodyRegion; |
| import org.apache.fop.area.PageViewport; |
| import org.apache.fop.fo.Constants; |
| 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.SimplePageMaster; |
| |
| /** |
| * <p>This class delivers Page instances. It also caches them as necessary. |
| * </p> |
| * <p>Additional functionality makes sure that surplus instances that are requested by the |
| * page breaker are properly discarded, especially in situations where hard breaks cause |
| * blank pages. The reason for that: The page breaker sometimes needs to preallocate |
| * additional pages since it doesn't know exactly until the end how many pages it really needs. |
| * </p> |
| */ |
| public class PageProvider implements Constants { |
| |
| private Log log = LogFactory.getLog(PageProvider.class); |
| |
| /** Indices are evaluated relative to the first page in the page-sequence. */ |
| public static final int RELTO_PAGE_SEQUENCE = 0; |
| /** Indices are evaluated relative to the first page in the current element list. */ |
| public static final int RELTO_CURRENT_ELEMENT_LIST = 1; |
| |
| private int startPageOfPageSequence; |
| private int startPageOfCurrentElementList; |
| private int startColumnOfCurrentElementList; |
| private boolean spanAllForCurrentElementList; |
| private List<Page> cachedPages = new java.util.ArrayList<Page>(); |
| |
| private int lastPageIndex = -1; |
| private int indexOfCachedLastPage = -1; |
| |
| //Cache to optimize getAvailableBPD() calls |
| private int lastRequestedIndex = -1; |
| private int lastReportedBPD = -1; |
| |
| /** |
| * AreaTreeHandler which activates the PSLM and controls |
| * the rendering of its pages. |
| */ |
| private AreaTreeHandler areaTreeHandler; |
| |
| /** |
| * fo:page-sequence formatting object being |
| * processed by this class |
| */ |
| private PageSequence pageSeq; |
| |
| protected boolean skipPagePositionOnly; |
| |
| /** |
| * Main constructor. |
| * @param ath the area tree handler |
| * @param ps The page-sequence the provider operates on |
| */ |
| public PageProvider(AreaTreeHandler ath, PageSequence ps) { |
| this.areaTreeHandler = ath; |
| this.pageSeq = ps; |
| this.startPageOfPageSequence = ps.getStartingPageNumber(); |
| } |
| |
| public void initialize() { |
| cachedPages.clear(); |
| } |
| |
| /** |
| * The page breaker notifies the provider about the page number an element list starts |
| * on so it can later retrieve PageViewports relative to this first page. |
| * @param startPage the number of the first page for the element list. |
| * @param startColumn the starting column number for the element list. |
| * @param spanAll true if the current element list is for a column-spanning section |
| */ |
| public void setStartOfNextElementList(int startPage, int startColumn, boolean spanAll) { |
| if (log.isDebugEnabled()) { |
| log.debug("start of the next element list is:" |
| + " page=" + startPage + " col=" + startColumn |
| + (spanAll ? ", column-spanning" : "")); |
| } |
| this.startPageOfCurrentElementList = startPage - startPageOfPageSequence + 1; |
| this.startColumnOfCurrentElementList = startColumn; |
| this.spanAllForCurrentElementList = spanAll; |
| //Reset Cache |
| this.lastRequestedIndex = -1; |
| this.lastReportedBPD = -1; |
| } |
| |
| /** |
| * Sets the index of the last page. This is done as soon as the position of the last page |
| * is known or assumed. |
| * @param index the index relative to the first page in the page-sequence |
| */ |
| public void setLastPageIndex(int index) { |
| this.lastPageIndex = index; |
| } |
| |
| /** |
| * Returns the available BPD for the part/page indicated by the index parameter. |
| * The index is the part/page relative to the start of the current element list. |
| * This method takes multiple columns into account. |
| * @param index zero-based index of the requested part/page |
| * @return the available BPD |
| */ |
| public int getAvailableBPD(int index) { |
| //Special optimization: There may be many equal calls by the BreakingAlgorithm |
| if (this.lastRequestedIndex == index) { |
| if (log.isTraceEnabled()) { |
| log.trace("getAvailableBPD(" + index + ") -> (cached) " + lastReportedBPD); |
| } |
| return this.lastReportedBPD; |
| } |
| int pageIndexTmp = index; |
| int pageIndex = 0; |
| int colIndex = startColumnOfCurrentElementList; |
| Page page = getPage( |
| false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); |
| while (pageIndexTmp > 0) { |
| colIndex++; |
| if (colIndex >= page.getPageViewport().getCurrentSpan().getColumnCount()) { |
| colIndex = 0; |
| pageIndex++; |
| page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); |
| BodyRegion br = page.getPageViewport().getBodyRegion(); |
| if (!pageSeq.getMainFlow().getFlowName().equals(br.getRegionName())) { |
| pageIndexTmp++; |
| } |
| } |
| pageIndexTmp--; |
| } |
| this.lastRequestedIndex = index; |
| this.lastReportedBPD = page.getPageViewport().getBodyRegion().getRemainingBPD(); |
| if (log.isTraceEnabled()) { |
| log.trace("getAvailableBPD(" + index + ") -> " + lastReportedBPD); |
| } |
| return this.lastReportedBPD; |
| } |
| |
| private static class Column { |
| |
| final Page page; |
| |
| final int pageIndex; |
| |
| final int colIndex; |
| |
| final int columnCount; |
| |
| Column(Page page, int pageIndex, int colIndex, int columnCount) { |
| this.page = page; |
| this.pageIndex = pageIndex; |
| this.colIndex = colIndex; |
| this.columnCount = columnCount; |
| } |
| |
| } |
| |
| private Column getColumn(int index) { |
| int columnCount = 0; |
| int colIndex = startColumnOfCurrentElementList + index; |
| int pageIndex = -1; |
| Page page; |
| do { |
| colIndex -= columnCount; |
| pageIndex++; |
| page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); |
| columnCount = page.getPageViewport().getCurrentSpan().getColumnCount(); |
| } while (colIndex >= columnCount); |
| return new Column(page, pageIndex, colIndex, columnCount); |
| } |
| |
| /** |
| * Compares the IPD of the given part with the following one. |
| * |
| * @param index index of the current part |
| * @return a negative integer, zero or a positive integer as the current IPD is less |
| * than, equal to or greater than the IPD of the following part |
| */ |
| public int compareIPDs(int index) { |
| Column column = getColumn(index); |
| if (column.colIndex + 1 < column.columnCount) { |
| // Next part is a column on same page => same IPD |
| return 0; |
| } else { |
| Page nextPage = getPage(false, column.pageIndex + 1, RELTO_CURRENT_ELEMENT_LIST); |
| return column.page.getPageViewport().getBodyRegion().getColumnIPD() |
| - nextPage.getPageViewport().getBodyRegion().getColumnIPD(); |
| } |
| } |
| |
| /** |
| * Checks if a break at the passed index would start a new page |
| * @param index the index of the element before the break |
| * @return {@code true} if the break starts a new page |
| */ |
| boolean startPage(int index) { |
| return getColumn(index).colIndex == 0; |
| } |
| |
| /** |
| * Checks if a break at the passed index would end a page |
| * @param index the index of the element before the break |
| * @return {@code true} if the break ends a page |
| */ |
| boolean endPage(int index) { |
| Column column = getColumn(index); |
| return column.colIndex == column.columnCount - 1; |
| } |
| |
| /** |
| * Obtain the applicable column-count for the element at the |
| * passed index |
| * @param index the index of the element |
| * @return the number of columns |
| */ |
| int getColumnCount(int index) { |
| return getColumn(index).columnCount; |
| } |
| |
| /** |
| * Returns the part index (0<x<partCount) which denotes the first part on the last page |
| * generated by the current element list. |
| * @param partCount Number of parts determined by the breaking algorithm |
| * @return the requested part index |
| */ |
| public int getStartingPartIndexForLastPage(int partCount) { |
| int lastPartIndex = partCount - 1; |
| return lastPartIndex - getColumn(lastPartIndex).colIndex; |
| } |
| |
| Page getPageFromColumnIndex(int columnIndex) { |
| return getColumn(columnIndex).page; |
| } |
| |
| /** |
| * Returns a Page. |
| * @param isBlank true if this page is supposed to be blank. |
| * @param index Index of the page (see relativeTo) |
| * @param relativeTo Defines which value the index parameter should be evaluated relative |
| * to. (One of PageProvider.RELTO_*) |
| * @return the requested Page |
| */ |
| public Page getPage(boolean isBlank, int index, int relativeTo) { |
| if (relativeTo == RELTO_PAGE_SEQUENCE) { |
| return getPage(isBlank, index); |
| } else if (relativeTo == RELTO_CURRENT_ELEMENT_LIST) { |
| int effIndex = startPageOfCurrentElementList + index; |
| effIndex += startPageOfPageSequence - 1; |
| return getPage(isBlank, effIndex); |
| } else { |
| throw new IllegalArgumentException( |
| "Illegal value for relativeTo: " + relativeTo); |
| } |
| } |
| |
| /** |
| * Returns a Page. |
| * @param isBlank true if the Page should be a blank one |
| * @param index the Page's index |
| * @return a Page instance |
| */ |
| protected Page getPage(boolean isBlank, int index) { |
| boolean isLastPage = (lastPageIndex >= 0) && (index == lastPageIndex); |
| if (log.isTraceEnabled()) { |
| log.trace("getPage(" + index + " " + (isBlank ? "blank" : "non-blank") |
| + (isLastPage ? " <LAST>" : "") + ")"); |
| } |
| int intIndex = index - startPageOfPageSequence; |
| if (log.isTraceEnabled()) { |
| if (isBlank) { |
| log.trace("blank page requested: " + index); |
| } |
| if (isLastPage) { |
| log.trace("last page requested: " + index); |
| } |
| } |
| if (intIndex > cachedPages.size()) { |
| throw new UnsupportedOperationException("Cannot handle holes in page cache"); |
| } else if (intIndex == cachedPages.size()) { |
| if (log.isTraceEnabled()) { |
| log.trace("Caching " + index); |
| } |
| cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList); |
| } |
| Page page = cachedPages.get(intIndex); |
| boolean replace = false; |
| if (page.getPageViewport().isBlank() != isBlank) { |
| log.debug("blank condition doesn't match. Replacing PageViewport."); |
| replace = true; |
| } |
| if (page.getPageViewport().getCurrentSpan().getColumnCount() == 1 |
| && !this.spanAllForCurrentElementList) { |
| RegionBody rb = (RegionBody)page.getSimplePageMaster().getRegion(Region.FO_REGION_BODY); |
| int colCount = rb.getColumnCount(); |
| if (colCount > 1) { |
| log.debug("Span doesn't match. Replacing PageViewport."); |
| replace = true; |
| } |
| } |
| if ((isLastPage && indexOfCachedLastPage != intIndex) |
| || (!isLastPage && indexOfCachedLastPage >= 0)) { |
| log.debug("last page condition doesn't match. Replacing PageViewport."); |
| replace = true; |
| indexOfCachedLastPage = (isLastPage ? intIndex : -1); |
| } |
| if (replace) { |
| discardCacheStartingWith(intIndex); |
| PageViewport oldPageVP = page.getPageViewport(); |
| page = cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList); |
| PageViewport newPageVP = page.getPageViewport(); |
| newPageVP.replace(oldPageVP); |
| this.areaTreeHandler.getIDTracker().replacePageViewPort(oldPageVP, newPageVP); |
| } |
| return page; |
| } |
| |
| protected void discardCacheStartingWith(int index) { |
| while (index < cachedPages.size()) { |
| this.cachedPages.remove(cachedPages.size() - 1); |
| if (!pageSeq.goToPreviousSimplePageMaster()) { |
| log.warn("goToPreviousSimplePageMaster() on the first page called!"); |
| } |
| } |
| } |
| |
| private Page cacheNextPage(int index, boolean isBlank, boolean isLastPage, boolean spanAll) { |
| String pageNumberString = pageSeq.makeFormattedPageNumber(index); |
| boolean isFirstPage = (startPageOfPageSequence == index); |
| SimplePageMaster spm = pageSeq.getNextSimplePageMaster( |
| index, isFirstPage, isLastPage, isBlank); |
| boolean isPagePositionOnly = pageSeq.hasPagePositionOnly() && !skipPagePositionOnly; |
| if (isPagePositionOnly) { |
| spm = pageSeq.getNextSimplePageMaster(index, isFirstPage, true, isBlank); |
| } |
| Page page = new Page(spm, index, pageNumberString, isBlank, spanAll, isPagePositionOnly); |
| //Set unique key obtained from the AreaTreeHandler |
| page.getPageViewport().setKey(areaTreeHandler.generatePageViewportKey()); |
| page.getPageViewport().setForeignAttributes(spm.getForeignAttributes()); |
| page.getPageViewport().setWritingModeTraits(pageSeq); |
| cachedPages.add(page); |
| if (isLastPage) { |
| pageSeq.getRoot().setLastSeq(pageSeq); |
| } else if (!isFirstPage) { |
| pageSeq.getRoot().setLastSeq(null); |
| } |
| return page; |
| } |
| |
| public int getIndexOfCachedLastPage() { |
| return indexOfCachedLastPage; |
| } |
| |
| public int getLastPageIndex() { |
| return lastPageIndex; |
| } |
| |
| public int getLastPageIPD() { |
| int index = this.cachedPages.size(); |
| boolean isFirstPage = (startPageOfPageSequence == index); |
| SimplePageMaster spm = pageSeq.getLastSimplePageMaster(index, isFirstPage, false); |
| Page page = new Page(spm, index, "", false, false, false); |
| if (pageSeq.getRoot().getLastSeq() != null && pageSeq.getRoot().getLastSeq() != pageSeq) { |
| return -1; |
| } |
| return page.getPageViewport().getBodyRegion().getColumnIPD(); |
| } |
| |
| public int getCurrentIPD() { |
| return getPageFromColumnIndex(startColumnOfCurrentElementList).getPageViewport().getBodyRegion() |
| .getColumnIPD(); |
| } |
| |
| /** |
| * Indicates whether the column/page at the given index is on the first page of the page sequence. |
| * |
| * @return {@code true} if the given part is on the first page of the sequence |
| */ |
| boolean isOnFirstPage(int partIndex) { |
| Column column = getColumn(partIndex); |
| return startPageOfCurrentElementList + column.pageIndex == startPageOfPageSequence; |
| } |
| |
| } |