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: RowPainter.java 1835810 2018-07-13 10:29:57Z ssteiner $ */
19 
20 package org.apache.fop.layoutmgr.table;
21 
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.ListIterator;
26 
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 
30 import org.apache.fop.area.Block;
31 import org.apache.fop.area.Trait;
32 import org.apache.fop.fo.flow.table.ConditionalBorder;
33 import org.apache.fop.fo.flow.table.EffRow;
34 import org.apache.fop.fo.flow.table.EmptyGridUnit;
35 import org.apache.fop.fo.flow.table.GridUnit;
36 import org.apache.fop.fo.flow.table.PrimaryGridUnit;
37 import org.apache.fop.fo.flow.table.Table;
38 import org.apache.fop.fo.flow.table.TableColumn;
39 import org.apache.fop.fo.flow.table.TablePart;
40 import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
41 import org.apache.fop.fo.properties.CommonBorderPaddingBackground.BorderInfo;
42 import org.apache.fop.layoutmgr.ElementListUtils;
43 import org.apache.fop.layoutmgr.KnuthElement;
44 import org.apache.fop.layoutmgr.KnuthPossPosIter;
45 import org.apache.fop.layoutmgr.LayoutContext;
46 import org.apache.fop.layoutmgr.SpaceResolver;
47 import org.apache.fop.layoutmgr.TraitSetter;
48 
49 class RowPainter {
50     private static Log log = LogFactory.getLog(RowPainter.class);
51     private int colCount;
52     private int currentRowOffset;
53     /** Currently handled row (= last encountered row). */
54     private EffRow currentRow;
55     private LayoutContext layoutContext;
56     /**
57      * Index of the first row of the current part present on the current page.
58      */
59     private int firstRowIndex;
60 
61     /**
62      * Index of the very first row on the current page. Needed to properly handle
63      * {@link BorderProps#COLLAPSE_OUTER}. This is not the same as {@link #firstRowIndex}
64      * when the table has headers!
65      */
66     private int firstRowOnPageIndex;
67 
68     /**
69      * Keeps track of the y-offsets of each row on a page.
70      * This is particularly needed for spanned cells where you need to know the y-offset
71      * of the starting row when the area is generated at the time the cell is closed.
72      */
73     private List rowOffsets = new ArrayList();
74 
75     private int[] cellHeights;
76     private boolean[] firstCellOnPage;
77     private CellPart[] firstCellParts;
78     private CellPart[] lastCellParts;
79 
80     /** y-offset of the current table part. */
81     private int tablePartOffset;
82     /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */
83     private CommonBorderPaddingBackground tablePartBackground;
84     /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */
85     private List tablePartBackgroundAreas;
86 
87     private TableContentLayoutManager tclm;
88 
RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext)89     RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext) {
90         this.tclm = tclm;
91         this.layoutContext = layoutContext;
92         this.colCount = tclm.getColumns().getColumnCount();
93         this.cellHeights = new int[colCount];
94         this.firstCellOnPage = new boolean[colCount];
95         this.firstCellParts = new CellPart[colCount];
96         this.lastCellParts = new CellPart[colCount];
97         this.firstRowIndex = -1;
98         this.firstRowOnPageIndex = -1;
99     }
100 
startTablePart(TablePart tablePart)101     void startTablePart(TablePart tablePart) {
102         CommonBorderPaddingBackground background = tablePart.getCommonBorderPaddingBackground();
103         if (background.hasBackground()) {
104             tablePartBackground = background;
105             if (tablePartBackgroundAreas == null) {
106                 tablePartBackgroundAreas = new ArrayList();
107             }
108         }
109         tablePartOffset = currentRowOffset;
110     }
111 
112     /**
113      * Signals that the end of the current table part is reached.
114      *
115      * @param lastInBody true if the part is the last table-body element to be displayed
116      * on the current page. In which case all the cells must be flushed even if they
117      * aren't finished, plus the proper collapsed borders must be selected (trailing
118      * instead of normal, or rest if the cell is unfinished)
119      * @param lastOnPage true if the part is the last to be displayed on the current page.
120      * In which case collapsed after borders for the cells on the last row must be drawn
121      * in the outer mode
122      */
endTablePart(boolean lastInBody, boolean lastOnPage)123     void endTablePart(boolean lastInBody, boolean lastOnPage) {
124         addAreasAndFlushRow(lastInBody, lastOnPage);
125 
126         if (tablePartBackground != null) {
127             TableLayoutManager tableLM = tclm.getTableLM();
128             for (Object tablePartBackgroundArea : tablePartBackgroundAreas) {
129                 Block backgroundArea = (Block) tablePartBackgroundArea;
130                 TraitSetter.addBackground(backgroundArea, tablePartBackground, tableLM,
131                         -backgroundArea.getXOffset(), tablePartOffset - backgroundArea.getYOffset(),
132                         tableLM.getContentAreaIPD(), currentRowOffset - tablePartOffset);
133             }
134             tablePartBackground = null;
135             tablePartBackgroundAreas.clear();
136         }
137     }
138 
getAccumulatedBPD()139     int getAccumulatedBPD() {
140         return currentRowOffset;
141     }
142 
143     /**
144      * Records the fragment of row represented by the given position. If it belongs to
145      * another (grid) row than the current one, that latter is painted and flushed first.
146      *
147      * @param tcpos a position representing the row fragment
148      */
handleTableContentPosition(TableContentPosition tcpos)149     void handleTableContentPosition(TableContentPosition tcpos) {
150         if (log.isDebugEnabled()) {
151             log.debug("===handleTableContentPosition(" + tcpos);
152         }
153         if (currentRow == null) {
154             currentRow = tcpos.getNewPageRow();
155         } else {
156             EffRow row = tcpos.getRow();
157             if (row.getIndex() > currentRow.getIndex()) {
158                 addAreasAndFlushRow(false, false);
159                 currentRow = row;
160             }
161         }
162         if (firstRowIndex < 0) {
163             firstRowIndex = currentRow.getIndex();
164             if (firstRowOnPageIndex < 0) {
165                 firstRowOnPageIndex = firstRowIndex;
166             }
167         }
168         //Iterate over all grid units in the current step
169         for (Object cellPart1 : tcpos.cellParts) {
170             CellPart cellPart = (CellPart) cellPart1;
171             if (log.isDebugEnabled()) {
172                 log.debug(">" + cellPart);
173             }
174             int colIndex = cellPart.pgu.getColIndex();
175             if (firstCellParts[colIndex] == null) {
176                 firstCellParts[colIndex] = cellPart;
177                 cellHeights[colIndex] = cellPart.getBorderPaddingBefore(firstCellOnPage[colIndex]);
178             } else {
179                 assert firstCellParts[colIndex].pgu == cellPart.pgu;
180                 cellHeights[colIndex] += cellPart.getConditionalBeforeContentLength();
181             }
182             cellHeights[colIndex] += cellPart.getLength();
183             lastCellParts[colIndex] = cellPart;
184         }
185     }
186 
187     /**
188      * Creates the areas corresponding to the last row. That is, an area with background
189      * for the row, plus areas for all the cells that finish on the row (not spanning over
190      * further rows).
191      *
192      * @param lastInPart true if the row is the last from its table part to be displayed
193      * on the current page. In which case all the cells must be flushed even if they
194      * aren't finished, plus the proper collapsed borders must be selected (trailing
195      * instead of normal, or rest if the cell is unfinished)
196      * @param lastOnPage true if the row is the very last row of the table that will be
197      * displayed on the current page. In which case collapsed after borders must be drawn
198      * in the outer mode
199      */
addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage)200     private void addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage) {
201         if (log.isDebugEnabled()) {
202             log.debug("Remembering yoffset for row " + currentRow.getIndex() + ": "
203                     + currentRowOffset);
204         }
205         recordRowOffset(currentRow.getIndex(), currentRowOffset);
206 
207         // Need to compute the actual row height first
208         // and determine border behaviour for empty cells
209         boolean firstCellPart = true;
210         boolean lastCellPart = true;
211         int actualRowHeight = 0;
212         for (int i = 0; i < colCount; i++) {
213             GridUnit currentGU = currentRow.getGridUnit(i);
214             if (currentGU.isEmpty()) {
215                 continue;
216             }
217             if (currentGU.getColSpanIndex() == 0
218                     && (lastInPart || currentGU.isLastGridUnitRowSpan())
219                     && firstCellParts[i] != null) {
220                 // TODO
221                 // The last test above is a workaround for the stepping algorithm's
222                 // fundamental flaw making it unable to produce the right element list for
223                 // multiple breaks inside a same row group.
224                 // (see http://wiki.apache.org/xmlgraphics-fop/TableLayout/KnownProblems)
225                 // In some extremely rare cases (forced breaks, very small page height), a
226                 // TableContentPosition produced during row delaying may end up alone on a
227                 // page. It will not contain the CellPart instances for the cells starting
228                 // the next row, so firstCellParts[i] will still be null for those ones.
229                 int cellHeight = cellHeights[i];
230                 cellHeight += lastCellParts[i].getConditionalAfterContentLength();
231                 cellHeight += lastCellParts[i].getBorderPaddingAfter(lastInPart);
232                 int cellOffset = getRowOffset(Math.max(firstCellParts[i].pgu.getRowIndex(),
233                         firstRowIndex));
234                 actualRowHeight = Math.max(actualRowHeight, cellOffset + cellHeight
235                         - currentRowOffset);
236             }
237 
238             if (firstCellParts[i] != null && !firstCellParts[i].isFirstPart()) {
239                 firstCellPart = false;
240             }
241             if (lastCellParts[i] != null && !lastCellParts[i].isLastPart()) {
242                 lastCellPart = false;
243             }
244         }
245 
246         // Then add areas for cells finishing on the current row
247         for (int i = 0; i < colCount; i++) {
248             GridUnit currentGU = currentRow.getGridUnit(i);
249             if (currentGU.isEmpty() && !tclm.isSeparateBorderModel()) {
250                 int borderBeforeWhich;
251                 if (firstCellPart) {
252                     if (firstCellOnPage[i]) {
253                         borderBeforeWhich = ConditionalBorder.LEADING_TRAILING;
254                     } else {
255                         borderBeforeWhich = ConditionalBorder.NORMAL;
256                     }
257                 } else {
258                     borderBeforeWhich = ConditionalBorder.REST;
259                 }
260                 int borderAfterWhich;
261                 if (lastCellPart) {
262                     if (lastInPart) {
263                         borderAfterWhich = ConditionalBorder.LEADING_TRAILING;
264                     } else {
265                         borderAfterWhich = ConditionalBorder.NORMAL;
266                     }
267                 } else {
268                     borderAfterWhich = ConditionalBorder.REST;
269                 }
270                 assert (currentGU instanceof EmptyGridUnit);
271                 addAreaForEmptyGridUnit((EmptyGridUnit)currentGU,
272                         currentRow.getIndex(), i,
273                         actualRowHeight,
274                         borderBeforeWhich, borderAfterWhich,
275                         lastOnPage);
276 
277                 firstCellOnPage[i] = false;
278             } else if (currentGU.getColSpanIndex() == 0
279                     && (lastInPart || currentGU.isLastGridUnitRowSpan())
280                     && firstCellParts[i] != null) {
281                 assert firstCellParts[i].pgu == currentGU.getPrimary();
282 
283                 int borderBeforeWhich;
284                 if (firstCellParts[i].isFirstPart()) {
285                     if (firstCellOnPage[i]) {
286                         borderBeforeWhich = ConditionalBorder.LEADING_TRAILING;
287                     } else {
288                         borderBeforeWhich = ConditionalBorder.NORMAL;
289                     }
290                 } else {
291                     assert firstCellOnPage[i];
292                     borderBeforeWhich = ConditionalBorder.REST;
293                 }
294                 int borderAfterWhich;
295                 if (lastCellParts[i].isLastPart()) {
296                     if (lastInPart) {
297                         borderAfterWhich = ConditionalBorder.LEADING_TRAILING;
298                     } else {
299                         borderAfterWhich = ConditionalBorder.NORMAL;
300                     }
301                 } else {
302                     borderAfterWhich = ConditionalBorder.REST;
303                 }
304 
305                 // when adding the areas for the TableCellLayoutManager this helps with the isLast trait
306                 // if, say, the first cell of a row has content that fits in the page, but the content of
307                 // the second cell does not fit this will assure that the isLast trait for the first cell
308                 // will also be false
309                 lastCellParts[i].pgu.getCellLM().setLastTrait(lastCellParts[i].isLastPart());
310                 addAreasForCell(firstCellParts[i].pgu,
311                         firstCellParts[i].start, lastCellParts[i].end,
312                         actualRowHeight, borderBeforeWhich, borderAfterWhich,
313                         lastOnPage);
314                 firstCellParts[i] = null; // why? what about the lastCellParts[i]?
315                 Arrays.fill(firstCellOnPage, i, i + currentGU.getCell().getNumberColumnsSpanned(),
316                         false);
317             }
318         }
319         currentRowOffset += actualRowHeight;
320         if (lastInPart) {
321             /*
322              * Either the end of the page is reached, then this was the last call of this
323              * method and we no longer care about currentRow; or the end of a table-part
324              * (header, footer, body) has been reached, and the next row will anyway be
325              * different from the current one, and this is unnecessary to call this method
326              * again in the first lines of handleTableContentPosition, so we may reset the
327              * following variables.
328              */
329             currentRow = null;
330             firstRowIndex = -1;
331             rowOffsets.clear();
332             /*
333              * The current table part has just been handled. Be it the first one or not,
334              * the header or the body, in any case the borders-before of the next row
335              * (i.e., the first row of the next part if any) must be painted in
336              * COLLAPSE_INNER mode. So the firstRowOnPageIndex indicator must be kept
337              * disabled. The following way is not the most elegant one but will be good
338              * enough.
339              */
340             firstRowOnPageIndex = Integer.MAX_VALUE;
341         }
342     }
343 
344     // TODO this is not very efficient and should probably be done another way
345     // this method is only necessary when display-align = center or after, in which case
346     // the exact content length is needed to compute the size of the empty block that will
347     // be used as padding.
348     // This should be handled automatically by a proper use of Knuth elements
computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex)349     private int computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex) {
350         if (startIndex > endIndex) {
351              // May happen if the cell contributes no content on the current page (empty
352              // cell, in most cases)
353             return 0;
354         } else {
355             ListIterator iter = pgu.getElements().listIterator(startIndex);
356             // Skip from the content length calculation glues and penalties occurring at the
357             // beginning of the page
358             boolean nextIsBox = false;
359             while (iter.nextIndex() <= endIndex && !nextIsBox) {
360                 nextIsBox = ((KnuthElement) iter.next()).isBox();
361             }
362             int len = 0;
363             if (((KnuthElement) iter.previous()).isBox()) {
364                 while (iter.nextIndex() < endIndex) {
365                     KnuthElement el = (KnuthElement) iter.next();
366                     if (el.isBox() || el.isGlue()) {
367                         len += el.getWidth();
368                     }
369                 }
370                 len += ActiveCell.getElementContentLength((KnuthElement) iter.next());
371             }
372             return len;
373         }
374     }
375 
addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos, int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage)376     private void addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos,
377             int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) {
378         /*
379          * Determine the index of the first row of this cell that will be displayed on the
380          * current page.
381          */
382         int currentRowIndex = currentRow.getIndex();
383         int startRowIndex;
384         int firstRowHeight;
385         if (pgu.getRowIndex() >= firstRowIndex) {
386             startRowIndex = pgu.getRowIndex();
387             if (startRowIndex < currentRowIndex) {
388                 firstRowHeight = getRowOffset(startRowIndex + 1) - getRowOffset(startRowIndex);
389             } else {
390                 firstRowHeight = rowHeight;
391             }
392         } else {
393             startRowIndex = firstRowIndex;
394             firstRowHeight = 0;
395         }
396 
397         /*
398          * In collapsing-border model, if the cell spans over several columns/rows then
399          * dedicated areas will be created for each grid unit to hold the corresponding
400          * borders. For that we need to know the height of each grid unit, that is of each
401          * grid row spanned over by the cell
402          */
403         int[] spannedGridRowHeights = null;
404         if (!tclm.getTableLM().getTable().isSeparateBorderModel() && pgu.hasSpanning()) {
405             spannedGridRowHeights = new int[currentRowIndex - startRowIndex + 1];
406             int prevOffset = getRowOffset(startRowIndex);
407             for (int i = 0; i < currentRowIndex - startRowIndex; i++) {
408                 int newOffset = getRowOffset(startRowIndex + i + 1);
409                 spannedGridRowHeights[i] = newOffset - prevOffset;
410                 prevOffset = newOffset;
411             }
412             spannedGridRowHeights[currentRowIndex - startRowIndex] = rowHeight;
413         }
414         int cellOffset = getRowOffset(startRowIndex);
415         int cellTotalHeight = rowHeight + currentRowOffset - cellOffset;
416         if (log.isDebugEnabled()) {
417             log.debug("Creating area for cell:");
418             log.debug("  start row: " + pgu.getRowIndex() + " " + currentRowOffset + " "
419                     + cellOffset);
420             log.debug(" rowHeight=" + rowHeight + " cellTotalHeight=" + cellTotalHeight);
421         }
422         TableCellLayoutManager cellLM = pgu.getCellLM();
423         cellLM.setXOffset(tclm.getXOffsetOfGridUnit(pgu));
424         cellLM.setYOffset(cellOffset);
425         cellLM.setContentHeight(computeContentLength(pgu, startPos, endPos));
426         cellLM.setTotalHeight(cellTotalHeight);
427         int prevBreak = ElementListUtils.determinePreviousBreak(pgu.getElements(), startPos);
428         if (endPos >= 0) {
429             SpaceResolver.performConditionalsNotification(pgu.getElements(),
430                     startPos, endPos, prevBreak);
431         }
432         cellLM.addAreas(new KnuthPossPosIter(pgu.getElements(), startPos, endPos + 1),
433                 layoutContext, spannedGridRowHeights, startRowIndex - pgu.getRowIndex(),
434                 currentRowIndex - pgu.getRowIndex(), borderBeforeWhich, borderAfterWhich,
435                 startRowIndex == firstRowOnPageIndex, lastOnPage, this, firstRowHeight);
436     }
437 
addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex, int actualRowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage)438     private void addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex,
439             int actualRowHeight,
440             int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) {
441 
442         //get effective borders
443         BorderInfo borderBefore = gu.getBorderBefore(borderBeforeWhich);
444         BorderInfo borderAfter = gu.getBorderAfter(borderAfterWhich);
445         BorderInfo borderStart = gu.getBorderStart();
446         BorderInfo borderEnd = gu.getBorderEnd();
447         if (borderBefore.getRetainedWidth() == 0
448                 && borderAfter.getRetainedWidth() == 0
449                 && borderStart.getRetainedWidth() == 0
450                 && borderEnd.getRetainedWidth() == 0) {
451             return; //no borders, no area necessary
452         }
453 
454         TableLayoutManager tableLM = tclm.getTableLM();
455         Table table = tableLM.getTable();
456         TableColumn col = tclm.getColumns().getColumn(colIndex + 1);
457 
458         //position information
459         boolean firstOnPage = (rowIndex == firstRowOnPageIndex);
460         boolean inFirstColumn = (colIndex == 0);
461         boolean inLastColumn = (colIndex == table.getNumberOfColumns() - 1);
462 
463         //determine the block area's size
464         int ipd = col.getColumnWidth().getValue(tableLM);
465         ipd -= (borderStart.getRetainedWidth() + borderEnd.getRetainedWidth()) / 2;
466         int bpd = actualRowHeight;
467         bpd -= (borderBefore.getRetainedWidth() + borderAfter.getRetainedWidth()) / 2;
468 
469         //generate the block area
470         Block block = new Block();
471         block.setChangeBarList(tclm.getTableLM().getFObj().getChangeBarList());
472         block.setPositioning(Block.ABSOLUTE);
473         block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE);
474         block.setIPD(ipd);
475         block.setBPD(bpd);
476         block.setXOffset(tclm.getXOffsetOfGridUnit(colIndex, 1)
477                 + (borderStart.getRetainedWidth() / 2));
478         block.setYOffset(getRowOffset(rowIndex)
479                 - (borderBefore.getRetainedWidth() / 2));
480         boolean[] outer = new boolean[] {firstOnPage, lastOnPage, inFirstColumn,
481                 inLastColumn};
482         TraitSetter.addCollapsingBorders(block,
483                 borderBefore,
484                 borderAfter,
485                 borderStart,
486                 borderEnd, outer);
487         tableLM.addChildArea(block);
488     }
489 
490     /**
491      * Registers the given area, that will be used to render the part of
492      * table-header/footer/body background covered by a table-cell. If percentages are
493      * used to place the background image, the final bpd of the (fraction of) table part
494      * that will be rendered on the current page must be known. The traits can't then be
495      * set when the areas for the cell are created since at that moment this bpd is yet
496      * unknown. So they will instead be set in
497      * {@link #addAreasAndFlushRow(boolean, boolean)}.
498      *
499      * @param backgroundArea the block of the cell's dimensions that will hold the part
500      * background
501      */
registerPartBackgroundArea(Block backgroundArea)502     void registerPartBackgroundArea(Block backgroundArea) {
503         tclm.getTableLM().addBackgroundArea(backgroundArea);
504         tablePartBackgroundAreas.add(backgroundArea);
505     }
506 
507     /**
508      * Records the y-offset of the row with the given index.
509      *
510      * @param rowIndex index of the row
511      * @param offset y-offset of the row on the page
512      */
recordRowOffset(int rowIndex, int offset)513     private void recordRowOffset(int rowIndex, int offset) {
514         /*
515          * In some very rare cases a row may be skipped. See for example Bugzilla #43633:
516          * in a two-column table, a row contains a row-spanning cell and a missing cell.
517          * In TableStepper#goToNextRowIfCurrentFinished this row will immediately be
518          * considered as finished, since it contains no cell ending on this row. Thus no
519          * TableContentPosition will be created for this row. Thus its index will never be
520          * recorded by the #handleTableContentPosition method.
521          *
522          * The offset of such a row is the same as the next non-empty row. It's needed
523          * to correctly offset blocks for cells starting on this row. Hence the loop
524          * below.
525          */
526         for (int i = rowOffsets.size(); i <= rowIndex - firstRowIndex; i++) {
527             rowOffsets.add(offset);
528         }
529     }
530 
531     /**
532      * Returns the offset of the row with the given index.
533      *
534      * @param rowIndex index of the row
535      * @return its y-offset on the page
536      */
getRowOffset(int rowIndex)537     private int getRowOffset(int rowIndex) {
538         return (Integer) rowOffsets.get(rowIndex - firstRowIndex);
539     }
540 
541     // TODO get rid of that
542     /** Signals that the first table-body instance has started. */
startBody()543     void startBody() {
544         Arrays.fill(firstCellOnPage, true);
545     }
546 
547     // TODO get rid of that
548     /** Signals that the last table-body instance has ended. */
endBody()549     void endBody() {
550         Arrays.fill(firstCellOnPage, false);
551     }
552 }
553