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&lt;x&lt;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