1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 /* $Id: PageProvider.java 1884905 2020-12-29 12:58:04Z ssteiner $ */ 19 20 package org.apache.fop.layoutmgr; 21 22 import java.util.List; 23 24 import org.apache.commons.logging.Log; 25 import org.apache.commons.logging.LogFactory; 26 27 import org.apache.fop.area.AreaTreeHandler; 28 import org.apache.fop.area.BodyRegion; 29 import org.apache.fop.area.PageViewport; 30 import org.apache.fop.fo.Constants; 31 import org.apache.fop.fo.pagination.PageSequence; 32 import org.apache.fop.fo.pagination.Region; 33 import org.apache.fop.fo.pagination.RegionBody; 34 import org.apache.fop.fo.pagination.SimplePageMaster; 35 36 /** 37 * <p>This class delivers Page instances. It also caches them as necessary. 38 * </p> 39 * <p>Additional functionality makes sure that surplus instances that are requested by the 40 * page breaker are properly discarded, especially in situations where hard breaks cause 41 * blank pages. The reason for that: The page breaker sometimes needs to preallocate 42 * additional pages since it doesn't know exactly until the end how many pages it really needs. 43 * </p> 44 */ 45 public class PageProvider implements Constants { 46 47 private Log log = LogFactory.getLog(PageProvider.class); 48 49 /** Indices are evaluated relative to the first page in the page-sequence. */ 50 public static final int RELTO_PAGE_SEQUENCE = 0; 51 /** Indices are evaluated relative to the first page in the current element list. */ 52 public static final int RELTO_CURRENT_ELEMENT_LIST = 1; 53 54 private int startPageOfPageSequence; 55 private int startPageOfCurrentElementList; 56 private int startColumnOfCurrentElementList; 57 private boolean spanAllForCurrentElementList; 58 private List<Page> cachedPages = new java.util.ArrayList<Page>(); 59 60 private int lastPageIndex = -1; 61 private int indexOfCachedLastPage = -1; 62 63 //Cache to optimize getAvailableBPD() calls 64 private int lastRequestedIndex = -1; 65 private int lastReportedBPD = -1; 66 67 /** 68 * AreaTreeHandler which activates the PSLM and controls 69 * the rendering of its pages. 70 */ 71 private AreaTreeHandler areaTreeHandler; 72 73 /** 74 * fo:page-sequence formatting object being 75 * processed by this class 76 */ 77 private PageSequence pageSeq; 78 79 protected boolean skipPagePositionOnly; 80 81 /** 82 * Main constructor. 83 * @param ath the area tree handler 84 * @param ps The page-sequence the provider operates on 85 */ PageProvider(AreaTreeHandler ath, PageSequence ps)86 public PageProvider(AreaTreeHandler ath, PageSequence ps) { 87 this.areaTreeHandler = ath; 88 this.pageSeq = ps; 89 this.startPageOfPageSequence = ps.getStartingPageNumber(); 90 } 91 initialize()92 public void initialize() { 93 cachedPages.clear(); 94 } 95 96 /** 97 * The page breaker notifies the provider about the page number an element list starts 98 * on so it can later retrieve PageViewports relative to this first page. 99 * @param startPage the number of the first page for the element list. 100 * @param startColumn the starting column number for the element list. 101 * @param spanAll true if the current element list is for a column-spanning section 102 */ setStartOfNextElementList(int startPage, int startColumn, boolean spanAll)103 public void setStartOfNextElementList(int startPage, int startColumn, boolean spanAll) { 104 if (log.isDebugEnabled()) { 105 log.debug("start of the next element list is:" 106 + " page=" + startPage + " col=" + startColumn 107 + (spanAll ? ", column-spanning" : "")); 108 } 109 this.startPageOfCurrentElementList = startPage - startPageOfPageSequence + 1; 110 this.startColumnOfCurrentElementList = startColumn; 111 this.spanAllForCurrentElementList = spanAll; 112 //Reset Cache 113 this.lastRequestedIndex = -1; 114 this.lastReportedBPD = -1; 115 } 116 117 /** 118 * Sets the index of the last page. This is done as soon as the position of the last page 119 * is known or assumed. 120 * @param index the index relative to the first page in the page-sequence 121 */ setLastPageIndex(int index)122 public void setLastPageIndex(int index) { 123 this.lastPageIndex = index; 124 } 125 126 /** 127 * Returns the available BPD for the part/page indicated by the index parameter. 128 * The index is the part/page relative to the start of the current element list. 129 * This method takes multiple columns into account. 130 * @param index zero-based index of the requested part/page 131 * @return the available BPD 132 */ getAvailableBPD(int index)133 public int getAvailableBPD(int index) { 134 //Special optimization: There may be many equal calls by the BreakingAlgorithm 135 if (this.lastRequestedIndex == index) { 136 if (log.isTraceEnabled()) { 137 log.trace("getAvailableBPD(" + index + ") -> (cached) " + lastReportedBPD); 138 } 139 return this.lastReportedBPD; 140 } 141 int pageIndexTmp = index; 142 int pageIndex = 0; 143 int colIndex = startColumnOfCurrentElementList; 144 Page page = getPage( 145 false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); 146 while (pageIndexTmp > 0) { 147 colIndex++; 148 if (colIndex >= page.getPageViewport().getCurrentSpan().getColumnCount()) { 149 colIndex = 0; 150 pageIndex++; 151 page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); 152 BodyRegion br = page.getPageViewport().getBodyRegion(); 153 if (!pageSeq.getMainFlow().getFlowName().equals(br.getRegionName())) { 154 pageIndexTmp++; 155 } 156 } 157 pageIndexTmp--; 158 } 159 this.lastRequestedIndex = index; 160 this.lastReportedBPD = page.getPageViewport().getBodyRegion().getRemainingBPD(); 161 if (log.isTraceEnabled()) { 162 log.trace("getAvailableBPD(" + index + ") -> " + lastReportedBPD); 163 } 164 return this.lastReportedBPD; 165 } 166 167 private static class Column { 168 169 final Page page; 170 171 final int pageIndex; 172 173 final int colIndex; 174 175 final int columnCount; 176 Column(Page page, int pageIndex, int colIndex, int columnCount)177 Column(Page page, int pageIndex, int colIndex, int columnCount) { 178 this.page = page; 179 this.pageIndex = pageIndex; 180 this.colIndex = colIndex; 181 this.columnCount = columnCount; 182 } 183 184 } 185 getColumn(int index)186 private Column getColumn(int index) { 187 int columnCount = 0; 188 int colIndex = startColumnOfCurrentElementList + index; 189 int pageIndex = -1; 190 Page page; 191 do { 192 colIndex -= columnCount; 193 pageIndex++; 194 page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST); 195 columnCount = page.getPageViewport().getCurrentSpan().getColumnCount(); 196 } while (colIndex >= columnCount); 197 return new Column(page, pageIndex, colIndex, columnCount); 198 } 199 200 /** 201 * Compares the IPD of the given part with the following one. 202 * 203 * @param index index of the current part 204 * @return a negative integer, zero or a positive integer as the current IPD is less 205 * than, equal to or greater than the IPD of the following part 206 */ compareIPDs(int index)207 public int compareIPDs(int index) { 208 Column column = getColumn(index); 209 if (column.colIndex + 1 < column.columnCount) { 210 // Next part is a column on same page => same IPD 211 return 0; 212 } else { 213 Page nextPage = getPage(false, column.pageIndex + 1, RELTO_CURRENT_ELEMENT_LIST); 214 return column.page.getPageViewport().getBodyRegion().getColumnIPD() 215 - nextPage.getPageViewport().getBodyRegion().getColumnIPD(); 216 } 217 } 218 219 /** 220 * Checks if a break at the passed index would start a new page 221 * @param index the index of the element before the break 222 * @return {@code true} if the break starts a new page 223 */ startPage(int index)224 boolean startPage(int index) { 225 return getColumn(index).colIndex == 0; 226 } 227 228 /** 229 * Checks if a break at the passed index would end a page 230 * @param index the index of the element before the break 231 * @return {@code true} if the break ends a page 232 */ endPage(int index)233 boolean endPage(int index) { 234 Column column = getColumn(index); 235 return column.colIndex == column.columnCount - 1; 236 } 237 238 /** 239 * Obtain the applicable column-count for the element at the 240 * passed index 241 * @param index the index of the element 242 * @return the number of columns 243 */ getColumnCount(int index)244 int getColumnCount(int index) { 245 return getColumn(index).columnCount; 246 } 247 248 /** 249 * Returns the part index (0<x<partCount) which denotes the first part on the last page 250 * generated by the current element list. 251 * @param partCount Number of parts determined by the breaking algorithm 252 * @return the requested part index 253 */ getStartingPartIndexForLastPage(int partCount)254 public int getStartingPartIndexForLastPage(int partCount) { 255 int lastPartIndex = partCount - 1; 256 return lastPartIndex - getColumn(lastPartIndex).colIndex; 257 } 258 getPageFromColumnIndex(int columnIndex)259 Page getPageFromColumnIndex(int columnIndex) { 260 return getColumn(columnIndex).page; 261 } 262 263 /** 264 * Returns a Page. 265 * @param isBlank true if this page is supposed to be blank. 266 * @param index Index of the page (see relativeTo) 267 * @param relativeTo Defines which value the index parameter should be evaluated relative 268 * to. (One of PageProvider.RELTO_*) 269 * @return the requested Page 270 */ getPage(boolean isBlank, int index, int relativeTo)271 public Page getPage(boolean isBlank, int index, int relativeTo) { 272 if (relativeTo == RELTO_PAGE_SEQUENCE) { 273 return getPage(isBlank, index); 274 } else if (relativeTo == RELTO_CURRENT_ELEMENT_LIST) { 275 int effIndex = startPageOfCurrentElementList + index; 276 effIndex += startPageOfPageSequence - 1; 277 return getPage(isBlank, effIndex); 278 } else { 279 throw new IllegalArgumentException( 280 "Illegal value for relativeTo: " + relativeTo); 281 } 282 } 283 284 /** 285 * Returns a Page. 286 * @param isBlank true if the Page should be a blank one 287 * @param index the Page's index 288 * @return a Page instance 289 */ getPage(boolean isBlank, int index)290 protected Page getPage(boolean isBlank, int index) { 291 boolean isLastPage = (lastPageIndex >= 0) && (index == lastPageIndex); 292 if (log.isTraceEnabled()) { 293 log.trace("getPage(" + index + " " + (isBlank ? "blank" : "non-blank") 294 + (isLastPage ? " <LAST>" : "") + ")"); 295 } 296 int intIndex = index - startPageOfPageSequence; 297 if (log.isTraceEnabled()) { 298 if (isBlank) { 299 log.trace("blank page requested: " + index); 300 } 301 if (isLastPage) { 302 log.trace("last page requested: " + index); 303 } 304 } 305 if (intIndex > cachedPages.size()) { 306 throw new UnsupportedOperationException("Cannot handle holes in page cache"); 307 } else if (intIndex == cachedPages.size()) { 308 if (log.isTraceEnabled()) { 309 log.trace("Caching " + index); 310 } 311 cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList); 312 } 313 Page page = cachedPages.get(intIndex); 314 boolean replace = false; 315 if (page.getPageViewport().isBlank() != isBlank) { 316 log.debug("blank condition doesn't match. Replacing PageViewport."); 317 replace = true; 318 } 319 if (page.getPageViewport().getCurrentSpan().getColumnCount() == 1 320 && !this.spanAllForCurrentElementList) { 321 RegionBody rb = (RegionBody)page.getSimplePageMaster().getRegion(Region.FO_REGION_BODY); 322 int colCount = rb.getColumnCount(); 323 if (colCount > 1) { 324 log.debug("Span doesn't match. Replacing PageViewport."); 325 replace = true; 326 } 327 } 328 if ((isLastPage && indexOfCachedLastPage != intIndex) 329 || (!isLastPage && indexOfCachedLastPage >= 0)) { 330 log.debug("last page condition doesn't match. Replacing PageViewport."); 331 replace = true; 332 indexOfCachedLastPage = (isLastPage ? intIndex : -1); 333 } 334 if (replace) { 335 discardCacheStartingWith(intIndex); 336 PageViewport oldPageVP = page.getPageViewport(); 337 page = cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList); 338 PageViewport newPageVP = page.getPageViewport(); 339 newPageVP.replace(oldPageVP); 340 this.areaTreeHandler.getIDTracker().replacePageViewPort(oldPageVP, newPageVP); 341 } 342 return page; 343 } 344 discardCacheStartingWith(int index)345 protected void discardCacheStartingWith(int index) { 346 while (index < cachedPages.size()) { 347 this.cachedPages.remove(cachedPages.size() - 1); 348 if (!pageSeq.goToPreviousSimplePageMaster()) { 349 log.warn("goToPreviousSimplePageMaster() on the first page called!"); 350 } 351 } 352 } 353 cacheNextPage(int index, boolean isBlank, boolean isLastPage, boolean spanAll)354 private Page cacheNextPage(int index, boolean isBlank, boolean isLastPage, boolean spanAll) { 355 String pageNumberString = pageSeq.makeFormattedPageNumber(index); 356 boolean isFirstPage = (startPageOfPageSequence == index); 357 SimplePageMaster spm = pageSeq.getNextSimplePageMaster( 358 index, isFirstPage, isLastPage, isBlank); 359 boolean isPagePositionOnly = pageSeq.hasPagePositionOnly() && !skipPagePositionOnly; 360 if (isPagePositionOnly) { 361 spm = pageSeq.getNextSimplePageMaster(index, isFirstPage, true, isBlank); 362 } 363 Page page = new Page(spm, index, pageNumberString, isBlank, spanAll, isPagePositionOnly); 364 //Set unique key obtained from the AreaTreeHandler 365 page.getPageViewport().setKey(areaTreeHandler.generatePageViewportKey()); 366 page.getPageViewport().setForeignAttributes(spm.getForeignAttributes()); 367 page.getPageViewport().setWritingModeTraits(pageSeq); 368 cachedPages.add(page); 369 if (isLastPage) { 370 pageSeq.getRoot().setLastSeq(pageSeq); 371 } else if (!isFirstPage) { 372 pageSeq.getRoot().setLastSeq(null); 373 } 374 return page; 375 } 376 getIndexOfCachedLastPage()377 public int getIndexOfCachedLastPage() { 378 return indexOfCachedLastPage; 379 } 380 getLastPageIndex()381 public int getLastPageIndex() { 382 return lastPageIndex; 383 } 384 getLastPageIPD()385 public int getLastPageIPD() { 386 int index = this.cachedPages.size(); 387 boolean isFirstPage = (startPageOfPageSequence == index); 388 SimplePageMaster spm = pageSeq.getLastSimplePageMaster(index, isFirstPage, false); 389 Page page = new Page(spm, index, "", false, false, false); 390 if (pageSeq.getRoot().getLastSeq() != null && pageSeq.getRoot().getLastSeq() != pageSeq) { 391 return -1; 392 } 393 return page.getPageViewport().getBodyRegion().getColumnIPD(); 394 } 395 getCurrentIPD()396 public int getCurrentIPD() { 397 Page page = getPageFromColumnIndex(startColumnOfCurrentElementList); 398 return page.getPageViewport().getBodyRegion().getColumnIPD(); 399 } 400 getNextIPD()401 public int getNextIPD() { 402 Page page = getPageFromColumnIndex(startColumnOfCurrentElementList + 1); 403 return page.getPageViewport().getBodyRegion().getColumnIPD(); 404 } 405 getCurrentColumnCount()406 public int getCurrentColumnCount() { 407 Page page = getPageFromColumnIndex(startColumnOfCurrentElementList); 408 return page.getPageViewport().getCurrentSpan().getColumnCount(); 409 } 410 411 /** 412 * Indicates whether the column/page at the given index is on the first page of the page sequence. 413 * 414 * @return {@code true} if the given part is on the first page of the sequence 415 */ isOnFirstPage(int partIndex)416 boolean isOnFirstPage(int partIndex) { 417 Column column = getColumn(partIndex); 418 return startPageOfCurrentElementList + column.pageIndex == startPageOfPageSequence; 419 } 420 421 } 422