All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.layout.element.Table Maven / Gradle / Ivy

There is a newer version: 9.0.0
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2024 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
 */
package com.itextpdf.layout.element;

import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.kernel.pdf.tagutils.AccessibilityProperties;
import com.itextpdf.kernel.pdf.tagutils.DefaultAccessibilityProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.exceptions.LayoutExceptionMessageConstant;
import com.itextpdf.layout.properties.BorderCollapsePropertyValue;
import com.itextpdf.layout.properties.CaptionSide;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.TableRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * A {@link Table} is a layout element that represents data in a two-dimensional
 * grid. It is filled with {@link Cell cells}, ordered in rows and columns.
 * 

* It is an implementation of {@link ILargeElement}, which means it can be flushed * to the canvas, in order to reclaim memory that is locked up. */ public class Table extends BlockElement

implements ILargeElement { protected DefaultAccessibilityProperties tagProperties; private List rows; private UnitValue[] columnWidths; private int currentColumn = 0; private int currentRow = -1; private Table header; private Table footer; private boolean skipFirstHeader; private boolean skipLastFooter; private boolean isComplete; private List lastAddedRowGroups; // Start number of the row "window" (range) that this table currently contain. // For large tables we might contain only a few rows, not all of them, other ones might have been flushed. private int rowWindowStart = 0; private Document document; private Cell[] lastAddedRow; private Div caption; /** * Constructs a {@code Table} with the preferable column widths. *
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables. * For large table 100% width and fixed layout set implicitly. *
* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param columnWidths preferable column widths in points. Values must be greater than or equal to zero, * otherwise it will be interpreted as undefined. * @param largeTable whether parts of the table will be written before all data is added. * Note, large table does not support auto layout, table width shall not be removed. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(float[] columnWidths, boolean largeTable) { if (columnWidths == null) { throw new IllegalArgumentException("The widths array in table constructor can not be null."); } if (columnWidths.length == 0) { throw new IllegalArgumentException("The widths array in table constructor can not have zero length."); } this.columnWidths = normalizeColumnWidths(columnWidths); initializeLargeTable(largeTable); initializeRows(); } /** * Constructs a {@code Table} with the preferable column widths. *
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables. * For large table 100% width and fixed layout set implicitly. *
* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param columnWidths preferable column widths, points and/or percents. Values must be greater than or equal to zero, * otherwise it will be interpreted as undefined. * @param largeTable whether parts of the table will be written before all data is added. * Note, large table does not support auto layout, table width shall not be removed. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(UnitValue[] columnWidths, boolean largeTable) { if (columnWidths == null) { throw new IllegalArgumentException("The widths array in table constructor can not be null."); } if (columnWidths.length == 0) { throw new IllegalArgumentException("The widths array in table constructor can not have zero length."); } this.columnWidths = normalizeColumnWidths(columnWidths); initializeLargeTable(largeTable); initializeRows(); } /** * Constructs a {@code Table} with the preferable column widths. *
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default. *
* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param columnWidths preferable column widths, points and/or percents. Values must be greater than or equal to zero, * otherwise it will be interpreted as undefined. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(UnitValue[] columnWidths) { this(columnWidths, false); } /** * Constructs a {@code Table} with the preferable column widths. *
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default. *
* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param pointColumnWidths preferable column widths in points. Values must be greater than or equal to zero, * otherwise it will be interpreted as undefined. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(float[] pointColumnWidths) { this(pointColumnWidths, false); } /** * Constructs a {@code Table} with specified number of columns. * The final column widths depend on selected table layout. *
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables. * For large table fixed layout set implicitly. *
* Since 7.1 table will have undefined column widths, that will be determined during layout. * In oder to set equal percent width as column width, use {@link UnitValue#createPercentArray(int)} *
* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param numColumns the number of columns, each column will have equal percent width. * @param largeTable whether parts of the table will be written before all data is added. * Note, large table does not support auto layout, table width shall not be removed. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(int numColumns, boolean largeTable) { if (numColumns <= 0) { throw new IllegalArgumentException("The number of columns in Table constructor must be greater than zero"); } this.columnWidths = normalizeColumnWidths(numColumns); initializeLargeTable(largeTable); initializeRows(); } /** * Constructs a {@code Table} with specified number of columns. * The final column widths depend on selected table layout. *
* Since 7.0.2 table layout was introduced. Auto layout is default, except large tables. * For large table fixed layout set implicitly. *

* Since 7.1 table will have undefined column widths, that will be determined during layout. * In oder to set equal percent width as column width, use {@link UnitValue#createPercentArray(int)} *

* Note, the eventual columns width depends on selected layout, table width, * cell's width, cell's min-widths, and cell's max-widths. * Table layout algorithm has the same behaviour as expected for CSS table-layout property, * where {@code columnWidths} is <colgroup>'s widths. * For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}. * * @param numColumns the number of columns, each column will have equal percent width. * @see #setAutoLayout() * @see #setFixedLayout() */ public Table(int numColumns) { this(numColumns, false); } /** * Set fixed layout. Analog of {@code table-layout:fixed} CSS property. * Note, the table must have width property, otherwise auto layout will be used. *

* Algorithm description *
* 1. Scan columns for width property and set it. All the rest columns get undefined value. * Column width includes borders and paddings. Columns have set in constructor, analog of {@code

} element in HTML. *
* 2. Scan the very first row of table for width property and set it to undefined columns. * Cell width has lower priority in comparing with column. Cell width doesn't include borders and paddings. *
* 2.1 If cell has colspan and all columns are undefined, each column will get equal width: {@code width/colspan}. *
* 2.2 If some columns already have width, equal remain (original width minus existed) width will be added {@code remainWidth/colspan} to each column. *
* 3. If sum of columns is less, than table width, there are two options: *
* 3.1. If undefined columns still exist, they will get the rest remaining width. *
* 3.2. Otherwise all columns will be expanded proportionally based on its width. *
* 4. If sum of columns is greater, than table width, nothing to do. * * @return this element. */ public Table setFixedLayout() { setProperty(Property.TABLE_LAYOUT, "fixed"); return this; } /** * Set auto layout. Analog of {@code table-layout:auto} CSS property.
* Note, large table does not support auto layout. *

* Algorithm principles. *
* 1. Column width cannot be less, than min-width of any cell in the column (calculated by layout). *
* 2. Specified table width has higher priority, than sum of column and cell widths. *
* 3. Percent value of cell and column width has higher priority, than point value. *
* 4. Cell width has higher priority, than column width. *
* 5. If column has no width, it will try to reach max-value (calculated by layout). * * @return this element. */ public Table setAutoLayout() { setProperty(Property.TABLE_LAYOUT, "auto"); return this; } /** * Set {@link Property#WIDTH} = 100%. * * @return this element */ public Table useAllAvailableWidth() { setProperty(Property.WIDTH, UnitValue.createPercentValue(100)); return this; } /** * Returns the column width for the specified column. * * @param column index of the column * @return the width of the column */ public UnitValue getColumnWidth(int column) { return columnWidths[column]; } /** * Returns the number of columns. * * @return the number of columns. */ public int getNumberOfColumns() { return columnWidths.length; } /** * Returns the number of rows. * * @return the number of rows. */ public int getNumberOfRows() { return rows.size(); } /** * Adds a new cell to the header of the table. * The header will be displayed in the top of every area of this table. * See also {@link #setSkipFirstHeader(boolean)}. * * @param headerCell a header cell to be added * @return this element */ public Table addHeaderCell(Cell headerCell) { ensureHeaderIsInitialized(); header.addCell(headerCell); return this; } /** * Adds a new cell with received blockElement as a content to the header of the table. * The header will be displayed in the top of every area of this table. * See also {@link #setSkipFirstHeader(boolean)}. * * @param blockElement an element to be added to a header cell * @param any IElement * @return this element */ public Table addHeaderCell(BlockElement blockElement) { ensureHeaderIsInitialized(); header.addCell(blockElement); return this; } /** * Adds a new cell with received image to the header of the table. * The header will be displayed in the top of every area of this table. * See also {@link #setSkipFirstHeader(boolean)}. * * @param image an element to be added to a header cell * @return this element */ public Table addHeaderCell(Image image) { ensureHeaderIsInitialized(); header.addCell(image); return this; } /** * Adds a new cell with received string as a content to the header of the table. * The header will be displayed in the top of every area of this table. * See also {@link #setSkipFirstHeader(boolean)}. * * @param content a string to be added to a header cell * @return this element */ public Table addHeaderCell(String content) { ensureHeaderIsInitialized(); header.addCell(content); return this; } /** * Gets the header of the table. The header is represented as a distinct table and might have its own properties. * * @return table header or {@code null}, if {@link #addHeaderCell(Cell)} hasn't been called. */ public Table getHeader() { return header; } /** * Adds a new cell to the footer of the table. * The footer will be displayed in the bottom of every area of this table. * See also {@link #setSkipLastFooter(boolean)}. * * @param footerCell a footer cell * @return this element */ public Table addFooterCell(Cell footerCell) { ensureFooterIsInitialized(); footer.addCell(footerCell); return this; } /** * Adds a new cell with received blockElement as a content to the footer of the table. * The footer will be displayed in the bottom of every area of this table. * See also {@link #setSkipLastFooter(boolean)}. * * @param blockElement an element to be added to a footer cell * @param IElement instance * @return this element */ public Table addFooterCell(BlockElement blockElement) { ensureFooterIsInitialized(); footer.addCell(blockElement); return this; } /** * Adds a new cell with received image as a content to the footer of the table. * The footer will be displayed in the bottom of every area of this table. * See also {@link #setSkipLastFooter(boolean)}. * * @param image an image to be added to a footer cell * @return this element */ public Table addFooterCell(Image image) { ensureFooterIsInitialized(); footer.addCell(image); return this; } /** * Adds a new cell with received string as a content to the footer of the table. * The footer will be displayed in the bottom of every area of this table. * See also {@link #setSkipLastFooter(boolean)}. * * @param content a content string to be added to a footer cell * @return this element */ public Table addFooterCell(String content) { ensureFooterIsInitialized(); footer.addCell(content); return this; } /** * Gets the footer of the table. The footer is represented as a distinct table and might have its own properties. * * @return table footer or {@code null}, if {@link #addFooterCell(Cell)} hasn't been called. */ public Table getFooter() { return footer; } /** * Tells you if the first header needs to be skipped (for instance if the * header says "continued from the previous page"). * * @return Value of property skipFirstHeader. */ public boolean isSkipFirstHeader() { return skipFirstHeader; } /** * Skips the printing of the first header. Used when printing tables in * succession belonging to the same printed table aspect. * * @param skipFirstHeader New value of property skipFirstHeader. * @return this element */ public Table setSkipFirstHeader(boolean skipFirstHeader) { this.skipFirstHeader = skipFirstHeader; return this; } /** * Tells you if the last footer needs to be skipped (for instance if the * footer says "continued on the next page") * * @return Value of property skipLastFooter. */ public boolean isSkipLastFooter() { return skipLastFooter; } /** * Skips the printing of the last footer. Used when printing tables in * succession belonging to the same printed table aspect. * * @param skipLastFooter New value of property skipLastFooter. * @return this element */ public Table setSkipLastFooter(boolean skipLastFooter) { this.skipLastFooter = skipLastFooter; return this; } /** Sets the table's caption. * * If there is no {@link Property#CAPTION_SIDE} set (note that it's an inheritable property), * {@link CaptionSide#TOP} will be used. * Also the {@link StandardRoles#CAPTION} will be set on the element. * * @param caption The element to be set as a caption. * @return this element */ public Table setCaption(Div caption) { this.caption = caption; if (null != caption) { ensureCaptionPropertiesAreSet(); } return this; } /** * Sets the table's caption and its caption side. * * Also the {@link StandardRoles#CAPTION} will be set on the element. * * @param caption The element to be set as a caption. * @param side The caption side to be set on the caption. ** @return this element */ public Table setCaption(Div caption, CaptionSide side) { if (null != caption) { caption.setProperty(Property.CAPTION_SIDE, side); } setCaption(caption); return this; } private void ensureCaptionPropertiesAreSet() { this.caption.getAccessibilityProperties().setRole(StandardRoles.CAPTION); } /** * Gets the table's caption. * * @return the table's caption. */ public Div getCaption() { return caption; } /** * Starts new row. This mean that next cell will be added at the beginning of next line. * * @return this element */ public Table startNewRow() { currentColumn = 0; currentRow++; if (currentRow >= rows.size()) { rows.add(new Cell[columnWidths.length]); } return this; } /** * Adds a new cell to the table. The implementation decides for itself which * row the cell will be placed on. * * @param cell {@code Cell} to add. * @return this element */ public Table addCell(Cell cell) { if (isComplete && null != lastAddedRow) { throw new PdfException(LayoutExceptionMessageConstant.CANNOT_ADD_CELL_TO_COMPLETED_LARGE_TABLE); } // Try to find first empty slot in table. // We shall not use colspan or rowspan, 1x1 will be enough. while (true) { if (currentColumn >= columnWidths.length || currentColumn == -1) { startNewRow(); } if (rows.get(currentRow - rowWindowStart)[currentColumn] != null) { currentColumn++; } else { break; } } childElements.add(cell); cell.updateCellIndexes(currentRow, currentColumn, columnWidths.length); // extend bottom grid of slots to fix rowspan while (currentRow - rowWindowStart + cell.getRowspan() > rows.size()) { rows.add(new Cell[columnWidths.length]); } // set cell to all region include colspan and rowspan, except not-null cells. // this will help to handle empty rows and columns. for (int i = currentRow; i < currentRow + cell.getRowspan(); i++) { Cell[] row = rows.get(i - rowWindowStart); for (int j = currentColumn; j < currentColumn + cell.getColspan(); j++) { if (row[j] == null) { row[j] = cell; } } } currentColumn += cell.getColspan(); return this; } /** * Adds a new cell with received blockElement as a content. * * @param blockElement a blockElement to add to the cell and then to the table * @param IElement instance * @return this element */ public Table addCell(BlockElement blockElement) { return addCell(new Cell().add(blockElement)); } /** * Adds a new cell with received image as a content. * * @param image an image to add to the cell and then to the table * @return this element */ public Table addCell(Image image) { return addCell(new Cell().add(image)); } /** * Adds a new cell with received string as a content. * * @param content a string to add to the cell and then to the table * @return this element */ public Table addCell(String content) { return addCell(new Cell().add(new Paragraph(content))); } /** * Returns a cell as specified by its location. If the cell is in a col-span * or row-span and is not the top left cell, then null is returned. * * @param row the row of the cell. indexes are zero-based * @param column the column of the cell. indexes are zero-based * @return the cell at the specified position. */ public Cell getCell(int row, int column) { if (row - rowWindowStart < rows.size()) { Cell cell = rows.get(row - rowWindowStart)[column]; // make sure that it is top left corner of cell, even in case colspan or rowspan if (cell != null && cell.getRow() == row && cell.getCol() == column) { return cell; } } return null; } /** * Creates a renderer subtree with root in the current table element. * Compared to {@link #getRenderer()}, the renderer returned by this method should contain all the child * renderers for children of the current element. * * @return a {@link TableRenderer} subtree for this element */ @Override public IRenderer createRendererSubTree() { TableRenderer rendererRoot = (TableRenderer) getRenderer(); for (IElement child : childElements) { boolean childShouldBeAdded = isComplete || cellBelongsToAnyRowGroup((Cell) child, lastAddedRowGroups); if (childShouldBeAdded) { rendererRoot.addChild(child.createRendererSubTree()); } } return rendererRoot; } /** * Gets a table renderer for this element. Note that this method can be called more than once. * By default each element should define its own renderer, but the renderer can be overridden by * {@link #setNextRenderer(IRenderer)} method call. * * @return a table renderer for this element */ @Override public IRenderer getRenderer() { if (nextRenderer != null) { if (nextRenderer instanceof TableRenderer) { IRenderer renderer = nextRenderer; nextRenderer = nextRenderer.getNextRenderer(); return renderer; } else { Logger logger = LoggerFactory.getLogger(Table.class); logger.error("Invalid renderer for Table: must be inherited from TableRenderer"); } } // In case of large tables, we only add to the renderer the cells from complete row groups, // for incomplete ones we may have problem with partial rendering because of cross-dependency. if (isComplete) { // if table was large we need to remove the last flushed group of rows, so we need to update lastAddedRowGroups if (null != lastAddedRow && 0 != rows.size()) { List allRows = new ArrayList<>(); allRows.add(new RowRange(rowWindowStart, rowWindowStart + rows.size() - 1)); lastAddedRowGroups = allRows; } } else { lastAddedRowGroups = getRowGroups(); } if (isComplete) { return new TableRenderer(this, new RowRange(rowWindowStart, rowWindowStart + rows.size() - 1)); } else { int rowWindowFinish = lastAddedRowGroups.size() != 0 ? lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).finishRow : -1; return new TableRenderer(this, new RowRange(rowWindowStart, rowWindowFinish)); } } @Override public boolean isComplete() { return isComplete; } /** * Indicates that all the desired content has been added to this large element and no more content will be added. * After this method is called, more precise rendering is activated. * For instance, a table may have a {@link #setSkipLastFooter(boolean)} method set to true, * and in case of large table on {@link #flush()} we do not know if any more content will be added, * so we might not place the content in the bottom of the page where it would fit, but instead add a footer, and * place that content in the start of the page. Technically such result would look all right, but it would be * more concise if we placed the content in the bottom and did not start new page. For such cases to be * renderered more accurately, one can call complete() when some content is still there and not flushed. */ @Override public void complete() { assert !isComplete; isComplete = true; flush(); } /** * Writes the newly added content to the document. */ @Override public void flush() { Cell[] row = null; int rowNum = rows.size(); if (!rows.isEmpty()) { row = rows.get(rows.size() - 1); } document.add(this); if (row != null && rowNum != rows.size()) { lastAddedRow = row; } } /** * Flushes the content which has just been added to the document. * This is a method for internal usage and is called automatically by the document. */ @Override public void flushContent() { if (lastAddedRowGroups == null || lastAddedRowGroups.isEmpty()) return; int firstRow = lastAddedRowGroups.get(0).startRow; int lastRow = lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).finishRow; List toRemove = new ArrayList<>(); for (IElement cell : childElements) { if (((Cell) cell).getRow() >= firstRow && ((Cell) cell).getRow() <= lastRow) { toRemove.add(cell); } } childElements.removeAll(toRemove); for (int i = 0; i < lastRow - firstRow; i++) { rows.remove(firstRow - rowWindowStart); } lastAddedRow = rows.remove(firstRow - rowWindowStart); rowWindowStart = lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).getFinishRow() + 1; lastAddedRowGroups = null; } @Override public void setDocument(Document document) { this.document = document; } /** * Gets the markup properties of the bottom border of the (current) last row. * * @return an array of {@link Border} objects */ public List getLastRowBottomBorder() { List horizontalBorder = new ArrayList<>(); if (lastAddedRow != null) { for (int i = 0; i < lastAddedRow.length; i++) { Cell cell = lastAddedRow[i]; Border border = null; if (cell != null) { if (cell.hasProperty(Property.BORDER_BOTTOM)) { border = cell.getProperty(Property.BORDER_BOTTOM); } else if (cell.hasProperty(Property.BORDER)) { border = cell.getProperty(Property.BORDER); } else { border = cell.getDefaultProperty(Property.BORDER); } } horizontalBorder.add(border); } } return horizontalBorder; } /** * Defines whether the {@link Table} should be extended to occupy all the space left in the available area * in case it is the last element in this area. * * @param isExtended defines whether the {@link Table} should be extended * @return this {@link Table} */ public Table setExtendBottomRow(boolean isExtended) { setProperty(Property.FILL_AVAILABLE_AREA, isExtended); return this; } /** * Defines whether the {@link Table} should be extended to occupy all the space left in the available area * in case the area has been split and it is the last element in the split part of this area. * * @param isExtended defines whether the {@link Table} should be extended * @return this {@link Table} */ public Table setExtendBottomRowOnSplit(boolean isExtended) { setProperty(Property.FILL_AVAILABLE_AREA_ON_SPLIT, isExtended); return this; } /** * Sets the type of border collapse. * * @param collapsePropertyValue {@link BorderCollapsePropertyValue} to be set as the border collapse type * @return this {@link Table} */ public Table setBorderCollapse(BorderCollapsePropertyValue collapsePropertyValue) { setProperty(Property.BORDER_COLLAPSE, collapsePropertyValue); if (null != header) { header.setBorderCollapse(collapsePropertyValue); } if (null != footer) { footer.setBorderCollapse(collapsePropertyValue); } return this; } /** * Sets the horizontal spacing between this {@link Table table}'s {@link Cell cells}. * * @param spacing a horizontal spacing between this {@link Table table}'s {@link Cell cells} * @return this {@link Table} */ public Table setHorizontalBorderSpacing(float spacing) { setProperty(Property.HORIZONTAL_BORDER_SPACING, spacing); if (null != header) { header.setHorizontalBorderSpacing(spacing); } if (null != footer) { footer.setHorizontalBorderSpacing(spacing); } return this; } /** * Sets the vertical spacing between this {@link Table table}'s {@link Cell cells}. * * @param spacing a vertical spacing between this {@link Table table}'s {@link Cell cells} * @return this {@link Table} */ public Table setVerticalBorderSpacing(float spacing) { setProperty(Property.VERTICAL_BORDER_SPACING, spacing); if (null != header) { header.setVerticalBorderSpacing(spacing); } if (null != footer) { footer.setVerticalBorderSpacing(spacing); } return this; } @Override public AccessibilityProperties getAccessibilityProperties() { if (tagProperties == null) { tagProperties = new DefaultAccessibilityProperties(StandardRoles.TABLE); } return tagProperties; } @Override protected IRenderer makeNewRenderer() { return new TableRenderer(this); } private static UnitValue[] normalizeColumnWidths(float[] pointColumnWidths) { UnitValue[] normalized = new UnitValue[pointColumnWidths.length]; for (int i = 0; i < normalized.length; i++) { if (pointColumnWidths[i] >= 0) { normalized[i] = UnitValue.createPointValue(pointColumnWidths[i]); } } return normalized; } private static UnitValue[] normalizeColumnWidths(UnitValue[] unitColumnWidths) { UnitValue[] normalized = new UnitValue[unitColumnWidths.length]; for (int i = 0; i < unitColumnWidths.length; i++) { normalized[i] = unitColumnWidths[i] != null && unitColumnWidths[i].getValue() >= 0 ? new UnitValue(unitColumnWidths[i]) : null; } return normalized; } private static UnitValue[] normalizeColumnWidths(int numberOfColumns) { UnitValue[] normalized = new UnitValue[numberOfColumns]; return normalized; } /** * Returns the list of all row groups. * * @return a list of a {@link RowRange} which holds the row numbers of a section of a table */ protected java.util.List getRowGroups() { int lastRowWeCanFlush = currentColumn == columnWidths.length ? currentRow : currentRow - 1; int[] cellBottomRows = new int[columnWidths.length]; int currentRowGroupStart = rowWindowStart; java.util.List rowGroups = new ArrayList<>(); while (currentRowGroupStart <= lastRowWeCanFlush) { for (int i = 0; i < columnWidths.length; i++) { cellBottomRows[i] = currentRowGroupStart; } int maxRowGroupFinish = cellBottomRows[0] + rows.get(cellBottomRows[0] - rowWindowStart)[0].getRowspan() - 1; boolean converged = false; boolean rowGroupComplete = true; while (!converged) { converged = true; for (int i = 0; i < columnWidths.length; i++) { while (cellBottomRows[i] < lastRowWeCanFlush && cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 < maxRowGroupFinish) { cellBottomRows[i] += rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan(); } if (cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 > maxRowGroupFinish) { maxRowGroupFinish = cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1; converged = false; } else if (cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 < maxRowGroupFinish) { // don't have enough cells for a row group yet. rowGroupComplete = false; } } } if (rowGroupComplete) { rowGroups.add(new RowRange(currentRowGroupStart, maxRowGroupFinish)); } currentRowGroupStart = maxRowGroupFinish + 1; } return rowGroups; } private void initializeRows() { rows = new ArrayList<>(); currentColumn = -1; } private boolean cellBelongsToAnyRowGroup(Cell cell, List rowGroups) { return rowGroups != null && rowGroups.size() > 0 && cell.getRow() >= rowGroups.get(0).getStartRow() && cell.getRow() <= rowGroups.get(rowGroups.size() - 1).getFinishRow(); } private void ensureHeaderIsInitialized() { if (header == null) { header = new Table(columnWidths); UnitValue width = getWidth(); if (width != null) header.setWidth(width); header.getAccessibilityProperties().setRole(StandardRoles.THEAD); if (hasOwnProperty(Property.BORDER_COLLAPSE)) { header.setBorderCollapse((BorderCollapsePropertyValue) this.getProperty(Property.BORDER_COLLAPSE)); } if (hasOwnProperty(Property.HORIZONTAL_BORDER_SPACING)) { header.setHorizontalBorderSpacing((float) this.getProperty(Property.HORIZONTAL_BORDER_SPACING)); } if (hasOwnProperty(Property.VERTICAL_BORDER_SPACING)) { header.setVerticalBorderSpacing((float) this.getProperty(Property.VERTICAL_BORDER_SPACING)); } } } private void ensureFooterIsInitialized() { if (footer == null) { footer = new Table(columnWidths); UnitValue width = getWidth(); if (width != null) footer.setWidth(width); footer.getAccessibilityProperties().setRole(StandardRoles.TFOOT); if (hasOwnProperty(Property.BORDER_COLLAPSE)) { footer.setBorderCollapse((BorderCollapsePropertyValue) this.getProperty(Property.BORDER_COLLAPSE)); } if (hasOwnProperty(Property.HORIZONTAL_BORDER_SPACING)) { footer.setHorizontalBorderSpacing((float) this.getProperty(Property.HORIZONTAL_BORDER_SPACING)); } if (hasOwnProperty(Property.VERTICAL_BORDER_SPACING)) { footer.setVerticalBorderSpacing((float) this.getProperty(Property.VERTICAL_BORDER_SPACING)); } } } private void initializeLargeTable(boolean largeTable) { this.isComplete = !largeTable; if (largeTable) { setWidth(UnitValue.createPercentValue(100)); setFixedLayout(); } } /** * A simple object which holds the row numbers of a section of a table. */ public static class RowRange { // The start number of the row group, inclusive int startRow; // The finish number of the row group, inclusive int finishRow; /** * Creates a {@link RowRange} * * @param startRow the start number of the row group, inclusive * @param finishRow the finish number of the row group, inclusive */ public RowRange(int startRow, int finishRow) { this.startRow = startRow; this.finishRow = finishRow; } /** * Gets the starting row number of the table section * * @return the start number of the row group, inclusive */ public int getStartRow() { return startRow; } /** * Gets the finishing row number of the table section * * @return the finish number of the row group, inclusive */ public int getFinishRow() { return finishRow; } } }