
com.itextpdf.tool.xml.html.table.Table Maven / Gradle / Ivy
/*
*
* This file is part of the iText (R) project.
Copyright (c) 1998-2022 iText Group NV
* Authors: Balder Van Camp, Emiel Ackermann, et al.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS.
*
* 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 http://www.gnu.org/licenses or
* write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA, 02110-1301 USA, or download the license from the following URL:
* http://itextpdf.com/terms-of-use/
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* In accordance with Section 7(b) of the GNU Affero General Public License, a
* covered work must retain the producer line in every PDF that is created or
* manipulated using iText.
*
* You can be released from the requirements of the license by purchasing a
* commercial license. Buying such a license is mandatory as soon as you develop
* commercial activities involving the iText software without disclosing the
* source code of your own applications. These activities include: offering paid
* services to customers as an ASP, serving PDFs on the fly in a web
* application, shipping iText with a closed source product.
*
* For more information, please contact iText Software Corp. at this address:
* [email protected]
*/
package com.itextpdf.tool.xml.html.table;
import com.itextpdf.text.*;
import com.itextpdf.text.html.HtmlUtilities;
import com.itextpdf.text.log.Level;
import com.itextpdf.text.log.Logger;
import com.itextpdf.text.log.LoggerFactory;
import com.itextpdf.text.pdf.*;
import com.itextpdf.tool.xml.NoCustomContextException;
import com.itextpdf.tool.xml.Tag;
import com.itextpdf.tool.xml.WorkerContext;
import com.itextpdf.tool.xml.css.*;
import com.itextpdf.tool.xml.exceptions.LocaleMessages;
import com.itextpdf.tool.xml.exceptions.RuntimeWorkerException;
import com.itextpdf.tool.xml.html.AbstractTagProcessor;
import com.itextpdf.tool.xml.html.HTML;
import com.itextpdf.tool.xml.html.pdfelement.HtmlCell;
import com.itextpdf.tool.xml.html.table.TableRowElement.Place;
import com.itextpdf.tool.xml.pipeline.html.HtmlPipelineContext;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
/**
* @author Emiel Ackermann
*
*/
public class Table extends AbstractTagProcessor {
public static final float DEFAULT_CELL_BORDER_WIDTH = 0.75f;
private static final Logger LOG = LoggerFactory.getLogger(Table.class);
private static final CssUtils utils = CssUtils.getInstance();
private static final FontSizeTranslator fst = FontSizeTranslator.getInstance();
/**
* Reorganizes table rows based on the designated normal {@link Place} in a
* table.
*
* @author Emiel Ackermann
*
*/
private final class NormalRowComparator implements Comparator {
public int compare(final TableRowElement o1, final TableRowElement o2) {
return o1.getPlace().getNormal().compareTo(o2.getPlace().getNormal());
}
}
/**
* Reorganizes table rows based on the designated repeated {@link Place} in
* a table.
*
* @author Emiel Ackermann
*
*/
private final class RepeatedRowComparator implements Comparator {
public int compare(final TableRowElement o1, final TableRowElement o2) {
return o1.getPlace().getRepeated().compareTo(o2.getPlace().getRepeated());
}
}
/**
* Default constructor.
*/
public Table() {
}
/*
* (non-Javadoc)
*
* @see
* com.itextpdf.tool.xml.TagProcessor#endElement(com.itextpdf.tool.xml.Tag,
* java.util.List, com.itextpdf.text.Document)
*/
@Override
public List end(final WorkerContext ctx, final Tag tag, final List currentContent) {
try {
boolean percentage = false;
String widthValue = tag.getCSS().get(HTML.Attribute.WIDTH);
if(widthValue == null) {
widthValue = tag.getAttributes().get(HTML.Attribute.WIDTH);
}
if(widthValue != null && widthValue.trim().endsWith("%")) {
percentage = true;
}
int numberOfColumns = 0;
List tableRows = new ArrayList(currentContent.size());
List invalidRowElements = new ArrayList(1);
String repeatHeader = tag.getCSS().get(CSS.Property.REPEAT_HEADER);
String repeatFooter = tag.getCSS().get(CSS.Property.REPEAT_FOOTER);
int headerRows = 0;
int footerRows = 0;
for (Element e : currentContent) {
int localNumCols = 0;
if (e instanceof TableRowElement) {
TableRowElement tableRowElement = (TableRowElement) e;
for (HtmlCell cell : tableRowElement.getContent()) {
localNumCols += cell.getColspan();
}
if (localNumCols > numberOfColumns) {
numberOfColumns = localNumCols;
}
tableRows.add(tableRowElement);
if (repeatHeader != null && repeatHeader.equalsIgnoreCase("yes")
&& tableRowElement.getPlace().equals(TableRowElement.Place.HEADER)) {
headerRows++;
}
if (repeatFooter != null && repeatFooter.equalsIgnoreCase("yes")
&& tableRowElement.getPlace().equals(TableRowElement.Place.FOOTER)) {
footerRows++;
}
} else {
invalidRowElements.add(e);
}
}
if (repeatFooter == null || !repeatFooter.equalsIgnoreCase("yes")) {
Collections.sort(tableRows, new NormalRowComparator());
} else {
Collections.sort(tableRows, new RepeatedRowComparator());
}
PdfPTable table = intPdfPTable(numberOfColumns);
table.setHeaderRows(headerRows + footerRows);
table.setFooterRows(footerRows);
if ( tag.getAttributes().containsKey(HTML.Attribute.ALIGN)) {
String value = tag.getAttributes().get(HTML.Attribute.ALIGN);
// TODO this property is inverted when RTL. so we should counter-invert here, probably.
table.setHorizontalAlignment(CSS.getElementAlignment(value));
}
int direction = getRunDirection(tag);
// if (direction != PdfWriter.RUN_DIRECTION_DEFAULT) {
table.setRunDirection(direction);
// }
for (Entry entry : tag.getCSS().entrySet()) {
if (entry.getKey().equalsIgnoreCase(CSS.Property.PAGE_BREAK_INSIDE)) {
if (entry.getValue().equalsIgnoreCase(CSS.Value.AVOID)) {
table.setKeepTogether(true);
}
}
}
TableStyleValues styleValues = setStyleValues(tag);
table.setTableEvent(new TableBorderEvent(styleValues));
setVerticalMargin(table, tag, styleValues, ctx);
widenLastCell(tableRows, styleValues.getHorBorderSpacing());
float[] columnWidths = new float[numberOfColumns];
float[] widestWords = new float[numberOfColumns];
float[] fixedWidths = new float[numberOfColumns];
float[] colspanWidestWords = new float[numberOfColumns];
int[] rowspanValue = new int[numberOfColumns];
float largestColumn = 0;
float largestColspanColumn = 0;
int indexOfLargestColumn = -1;
int indexOfLargestColspanColumn = -1;
// Initial fill of the widths arrays
for (TableRowElement row : tableRows) {
int column = 0;
for (HtmlCell cell : row.getContent()) {
// check whether the current column should be skipped due to a
// rowspan value of higher cell in this column.
// Contribution made by Arnost Havelka (Asseco): added while condition
while ((column < numberOfColumns) && (rowspanValue[column] > 0)) {
rowspanValue[column] = rowspanValue[column] - 1;
++column;
}
// sets a rowspan counter for current column (counter not
// needed for last column).
if (cell.getRowspan() > 1 && column != numberOfColumns - 1 && column < rowspanValue.length) {
rowspanValue[column] = cell.getRowspan() - 1;
}
int colspan = cell.getColspan();
if (cell.getFixedWidth() != 0) {
float fixedWidth = cell.getFixedWidth() + getCellStartWidth(cell);
float colSpanWidthSum = 0;
int nonZeroColspanCols = 0;
// Contribution made by Arnost Havelka (Asseco) (modified)
for (int c = column; c < column + colspan && c < numberOfColumns; c++) {
colSpanWidthSum += fixedWidths[c];
if (fixedWidths[c] != 0)
nonZeroColspanCols++;
}
for (int c = column; c < column + colspan && c < numberOfColumns; c++) {
if (fixedWidths[c] == 0) {
fixedWidths[c] = (fixedWidth - colSpanWidthSum) / (colspan - nonZeroColspanCols);
columnWidths[c] = (fixedWidth - colSpanWidthSum) / (colspan - nonZeroColspanCols);
}
}
}
if (cell.getCompositeElements() != null) {
float[] widthValues = setCellWidthAndWidestWord(cell);
float cellWidth = widthValues[0] / colspan;
float widestWordOfCell = widthValues[1] / colspan;
for (int i = 0; i < colspan; i++) {
int c = column + i;
// Contribution made by Arnost Havelka (Asseco)
if (c >= numberOfColumns) {
continue;
}
if (fixedWidths[c] == 0 && cellWidth > columnWidths[c]) {
columnWidths[c] = cellWidth;
if (colspan == 1) {
if (cellWidth > largestColumn) {
largestColumn = cellWidth;
indexOfLargestColumn = c;
}
} else {
if (cellWidth > largestColspanColumn) {
largestColspanColumn = cellWidth;
indexOfLargestColspanColumn = c;
}
}
}
if (colspan == 1) {
if (widestWordOfCell > widestWords[c]) {
widestWords[c] = widestWordOfCell;
}
} else {
if (widestWordOfCell > colspanWidestWords[c]) {
colspanWidestWords[c] = widestWordOfCell;
}
}
}
}
if (colspan > 1) {
if (LOG.isLogging(Level.TRACE)) {
LOG.trace(String.format(LocaleMessages.getInstance().getMessage(LocaleMessages.COLSPAN),
colspan));
}
column += colspan - 1;
}
column++;
}
}
if (indexOfLargestColumn == -1) {
indexOfLargestColumn = indexOfLargestColspanColumn;
if (indexOfLargestColumn == -1) {
indexOfLargestColumn = 0;
}
for (int column = 0; column < numberOfColumns; column++) {
widestWords[column] = colspanWidestWords[column];
}
}
float outerWidth = getTableOuterWidth(tag, styleValues.getHorBorderSpacing(), ctx);
float initialTotalWidth = getTableWidth(columnWidths, 0);
// float targetWidth = calculateTargetWidth(tag, columnWidths, outerWidth, ctx);
float targetWidth = 0;
HtmlPipelineContext htmlPipelineContext = getHtmlPipelineContext(ctx);
float max = htmlPipelineContext.getPageSize().getWidth() - outerWidth;
boolean tableWidthFixed = false;
if (tag.getAttributes().get(CSS.Property.WIDTH) != null || tag.getCSS().get(CSS.Property.WIDTH) != null) {
targetWidth = new WidthCalculator().getWidth(tag, htmlPipelineContext.getRootTags(), htmlPipelineContext.getPageSize().getWidth(), initialTotalWidth);
if (targetWidth > max) {
targetWidth = max;
}
tableWidthFixed = true;
} else if (initialTotalWidth <= max) {
targetWidth = initialTotalWidth;
} else if (null == tag.getParent() || (null != tag.getParent() && htmlPipelineContext.getRootTags().contains(tag.getParent().getName()))) {
targetWidth = max;
} else /* this table is an inner table and width adjustment is done in outer table */{
targetWidth = getTableWidth(columnWidths, outerWidth);
}
float totalFixedColumnWidth = getTableWidth(fixedWidths, 0);
float targetPercentage = 0;
if (totalFixedColumnWidth == initialTotalWidth) { // all column widths are fixed
targetPercentage = targetWidth / initialTotalWidth;
if (initialTotalWidth > targetWidth) {
for (int column = 0; column < columnWidths.length; column++) {
columnWidths[column] *= targetPercentage;
}
} else if(tableWidthFixed && targetPercentage != 1){
for (int column = 0; column < columnWidths.length; column++) {
columnWidths[column] *= targetPercentage;
}
}
} else {
targetPercentage = (targetWidth - totalFixedColumnWidth) / (initialTotalWidth - totalFixedColumnWidth);
// Reduce width of columns if the columnWidth array + borders +
// paddings
// is too large for the given targetWidth.
if (initialTotalWidth > targetWidth) {
float leftToReduce = 0;
for (int column = 0; column < columnWidths.length; column++) {
if (fixedWidths[column] == 0) {
// Reduce width of the column to its targetWidth, if
// widestWord of column still fits in the
// targetWidth of
// the
// column.
if (widestWords[column] <= columnWidths[column] * targetPercentage) {
columnWidths[column] *= targetPercentage;
// else take the widest word and calculate space
// left to
// reduce.
} else {
columnWidths[column] = widestWords[column];
leftToReduce += widestWords[column] - columnWidths[column] * targetPercentage;
}
// if widestWord of a column does not fit in the
// fixedWidth,
// set the column width to the widestWord.
} else if (fixedWidths[column] < widestWords[column]) {
columnWidths[column] = widestWords[column];
leftToReduce += widestWords[column] - fixedWidths[column];
}
}
if (leftToReduce != 0) {
// Reduce width of the column with the most text, if its
// widestWord still fits in the reduced column.
if (widestWords[indexOfLargestColumn] <= columnWidths[indexOfLargestColumn] - leftToReduce) {
columnWidths[indexOfLargestColumn] -= leftToReduce;
} else { // set all none fixed columns to their minimum, with the
// widestWord array.
for (int column = 0; leftToReduce != 0 && column < columnWidths.length; column++) {
if (fixedWidths[column] == 0 && columnWidths[column] > widestWords[column]) {
float difference = columnWidths[column] - widestWords[column];
if (difference <= leftToReduce) {
leftToReduce -= difference;
columnWidths[column] = widestWords[column];
} else {
columnWidths[column] -= leftToReduce;
leftToReduce = 0;
}
}
}
if (leftToReduce != 0) {
// If the table has an insufficient fixed width
// by
// an
// attribute or style, try to enlarge the table
// to
// its
// minimum width (= widestWords array).
float pageWidth = getHtmlPipelineContext(ctx).getPageSize().getWidth();
if (getTableWidth(widestWords, outerWidth) < pageWidth) {
targetWidth = getTableWidth(widestWords, outerWidth);
leftToReduce = 0;
} else {
// If all columnWidths are set to the
// widestWordWidths and the table is still
// to
// wide
// content will fall off the edge of a page,
// which
// is similar to HTML.
targetWidth = pageWidth - outerWidth;
leftToReduce = 0;
}
}
}
}
// Enlarge width of columns to fit the targetWidth.
} else if (initialTotalWidth < targetWidth) {
for (int column = 0; column < columnWidths.length; column++) {
if (fixedWidths[column] == 0) {
columnWidths[column] *= targetPercentage;
}
}
}
}
try {
table.setTotalWidth(columnWidths);
table.setLockedWidth(true);
table.getDefaultCell().setBorder(Rectangle.NO_BORDER);
} catch (DocumentException e) {
throw new RuntimeWorkerException(LocaleMessages.getInstance().getMessage(
LocaleMessages.NO_CUSTOM_CONTEXT), e);
}
Float tableHeight = new HeightCalculator().getHeight(tag, getHtmlPipelineContext(ctx).getPageSize().getHeight());
Float tableRowHeight = null;
if (tableHeight != null && tableHeight > 0) tableRowHeight = tableHeight/tableRows.size();
int rowNumber = 0;
for (TableRowElement row : tableRows) {
int columnNumber = -1;
Float computedRowHeight = null;
/*if (tableHeight != null && tableRows.indexOf(row) == tableRows.size() - 1) {
float computedTableHeigt = table.calculateHeights();
computedRowHeight = tableHeight - computedTableHeigt;
}*/
List rowContent = row.getContent();
if (rowContent.size() < 1)
continue;
for (HtmlCell cell : rowContent) {
List compositeElements = cell.getCompositeElements();
if (compositeElements != null) {
for (Element baseLevel : compositeElements) {
if (baseLevel instanceof PdfPTable) {
TableStyleValues cellValues = cell.getCellValues();
float totalBordersWidth = cellValues.isLastInRow() ? styleValues
.getHorBorderSpacing() * 2
: styleValues.getHorBorderSpacing();
totalBordersWidth += cellValues.getBorderWidthLeft()
+ cellValues.getBorderWidthRight();
float columnWidth = 0;
for (int currentColumnNumber = columnNumber + 1 ;currentColumnNumber <= columnNumber + cell.getColspan(); currentColumnNumber++ ) {
columnWidth += columnWidths[currentColumnNumber];
}
PdfPTableEvent tableEvent = ((PdfPTable) baseLevel).getTableEvent();
TableStyleValues innerStyleValues = ((TableBorderEvent) tableEvent)
.getTableStyleValues();
totalBordersWidth += innerStyleValues.getBorderWidthLeft();
totalBordersWidth += innerStyleValues.getBorderWidthRight();
((PdfPTable) baseLevel).setTotalWidth(columnWidth - totalBordersWidth);
}
}
}
columnNumber += cell.getColspan();
table.addCell(cell);
}
table.completeRow();
if ((computedRowHeight == null || computedRowHeight <= 0) && tableRowHeight != null)
computedRowHeight = tableRowHeight;
if (computedRowHeight != null && computedRowHeight > 0) {
float rowHeight = table.getRow(rowNumber).getMaxHeights();
if (rowHeight < computedRowHeight) {
table.getRow(rowNumber).setMaxHeights(computedRowHeight);
} else if (tableRowHeight != null && tableRowHeight < rowHeight){
tableRowHeight = (tableHeight - rowHeight - rowNumber*tableRowHeight)
/(tableRows.size() - rowNumber - 1);
}
}
rowNumber++;
}
if (percentage) {
table.setWidthPercentage(utils.parsePxInCmMmPcToPt(widthValue));
table.setLockedWidth(false);
}
List elems = new ArrayList();
if (invalidRowElements.size() > 0) {
// all invalid row elements taken as caption
int i = 0;
Tag captionTag = tag.getChildren().get(i++);
while (!captionTag.getName().equalsIgnoreCase(HTML.Tag.CAPTION) && i < tag.getChildren().size()) {
captionTag = tag.getChildren().get(i);
i++;
}
String captionSideValue = captionTag.getCSS().get(CSS.Property.CAPTION_SIDE);
if (captionSideValue != null && captionSideValue.equalsIgnoreCase(CSS.Value.BOTTOM)) {
elems.add(table);
elems.addAll(invalidRowElements);
} else {
elems.addAll(invalidRowElements);
elems.add(table);
}
} else {
elems.add(table);
}
return elems;
} catch (NoCustomContextException e) {
throw new RuntimeWorkerException(LocaleMessages.getInstance().getMessage(LocaleMessages.NO_CUSTOM_CONTEXT), e);
}
}
protected PdfPTable intPdfPTable(int numberOfColumn) {
PdfPTable table = new PdfPTable(numberOfColumn);
table.setHorizontalAlignment(Element.ALIGN_LEFT);
table.setSplitLate(false);
return table;
}
/**
* Calculates the target width. First checks:
*
* - if the attribute or style "width" is found in the given tag and it is not wider than pageWidth - outerWidth, then the
* targetWidth = width value
* - if the columnWidths array in total is not wider than pageWidth - outerWidth, then the
* targetWidth = the total of the columnWidths array
* - if table's parent is a root tag or table has no parent, then the
* targetWidth = width of the page - outerWidth
* {@link Table#getTableOuterWidth(Tag, float, WorkerContext)}.
*
* If none of the above is true, the width of the table is set to its
* default with the columnWidths array.
*
* @param tag containing attributes and css.
* @param columnWidths float[] containing the widest lines of text found in
* the columns.
* @param outerWidth width needed for margins and borders.
* @param ctx
* @return float the target width of a table.
* @throws NoCustomContextException
*/
@SuppressWarnings("unused")
private float calculateTargetWidth(final Tag tag, final float[] columnWidths, final float outerWidth,
final WorkerContext ctx) throws NoCustomContextException {
float targetWidth = 0;
HtmlPipelineContext htmlPipelineContext = getHtmlPipelineContext(ctx);
float max = htmlPipelineContext.getPageSize().getWidth() - outerWidth;
float start = getTableWidth(columnWidths, 0);
if (tag.getAttributes().get(CSS.Property.WIDTH) != null || tag.getCSS().get(CSS.Property.WIDTH) != null) {
targetWidth = new WidthCalculator().getWidth(tag, htmlPipelineContext.getRootTags(), htmlPipelineContext
.getPageSize().getWidth());
if (targetWidth > max) {
targetWidth = max;
}
} else if (start <= max) {
targetWidth = start;
} else if (null == tag.getParent()
|| (null != tag.getParent() && htmlPipelineContext.getRootTags().contains(tag.getParent().getName()))) {
targetWidth = max;
} else /*
* this table is an inner table and width adjustment is done in
* outer table
*/{
targetWidth = getTableWidth(columnWidths, outerWidth);
}
return targetWidth;
}
/**
* Adds horizontal border spacing to the right padding of the last cell of
* each row.
*
* @param tableRows List of {@link TableRowElement} objects of the table.
* @param horBorderSpacing float containing the horizontal border spacing of
* the table.
*/
private void widenLastCell(final List tableRows, final float horBorderSpacing) {
for (TableRowElement row : tableRows) {
List cells = row.getContent();
if (cells.size() < 1)
continue;
HtmlCell last = cells.get(cells.size() - 1);
last.getCellValues().setLastInRow(true);
last.setPaddingRight(last.getPaddingRight() + horBorderSpacing);
}
}
/**
* Set the table style values in a {@link TableStyleValues} object based on
* attributes and css of the given tag.
*
* @param tag containing attributes and css.
* @return a {@link TableStyleValues} object containing the table's style
* values.
*/
public static TableStyleValues setStyleValues(final Tag tag) {
TableStyleValues styleValues = new TableStyleValues();
Map css = tag.getCSS();
Map attributes = tag.getAttributes();
if (attributes.containsKey(CSS.Property.BORDER)) {
styleValues.setBorderColor(BaseColor.BLACK);
String borderValue = attributes.get(CSS.Property.BORDER);
if ("".equals(borderValue))
styleValues.setBorderWidth(DEFAULT_CELL_BORDER_WIDTH);
else
styleValues.setBorderWidth(utils.parsePxInCmMmPcToPt(borderValue));
} else {
for (Entry entry : css.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key.equalsIgnoreCase(CSS.Property.BORDER_LEFT_STYLE) && CSS.Value.SOLID.equalsIgnoreCase(value)) {
styleValues.setBorderColorLeft(BaseColor.BLACK);
styleValues.setBorderWidthLeft(DEFAULT_CELL_BORDER_WIDTH);
} else if (key.equalsIgnoreCase(CSS.Property.BORDER_RIGHT_STYLE) && CSS.Value.SOLID.equalsIgnoreCase(value)) {
styleValues.setBorderColorRight(BaseColor.BLACK);
styleValues.setBorderWidthRight(DEFAULT_CELL_BORDER_WIDTH);
} else if (key.equalsIgnoreCase(CSS.Property.BORDER_TOP_STYLE) && CSS.Value.SOLID.equalsIgnoreCase(value)) {
styleValues.setBorderColorTop(BaseColor.BLACK);
styleValues.setBorderWidthTop(DEFAULT_CELL_BORDER_WIDTH);
} else if (key.equalsIgnoreCase(CSS.Property.BORDER_BOTTOM_STYLE) && CSS.Value.SOLID.equalsIgnoreCase(value)) {
styleValues.setBorderColorBottom(BaseColor.BLACK);
styleValues.setBorderWidthBottom(DEFAULT_CELL_BORDER_WIDTH);
}
}
String color = css.get(CSS.Property.BORDER_BOTTOM_COLOR);
if (color != null) {
styleValues.setBorderColorBottom(HtmlUtilities.decodeColor(color));
}
color = css.get(CSS.Property.BORDER_TOP_COLOR);
if (color != null) {
styleValues.setBorderColorTop(HtmlUtilities.decodeColor(color));
}
color = css.get(CSS.Property.BORDER_LEFT_COLOR);
if (color != null) {
styleValues.setBorderColorLeft(HtmlUtilities.decodeColor(color));
}
color = css.get(CSS.Property.BORDER_RIGHT_COLOR);
if (color != null) {
styleValues.setBorderColorRight(HtmlUtilities.decodeColor(color));
}
Float width = utils.checkMetricStyle(css, CSS.Property.BORDER_BOTTOM_WIDTH);
if (width != null) {
styleValues.setBorderWidthBottom(width);
}
width = utils.checkMetricStyle(css, CSS.Property.BORDER_TOP_WIDTH);
if (width != null) {
styleValues.setBorderWidthTop(width);
}
width = utils.checkMetricStyle(css, CSS.Property.BORDER_RIGHT_WIDTH);
if (width != null) {
styleValues.setBorderWidthRight(width);
}
width = utils.checkMetricStyle(css, CSS.Property.BORDER_LEFT_WIDTH);
if (width != null) {
styleValues.setBorderWidthLeft(width);
}
}
styleValues.setBackground(HtmlUtilities.decodeColor(css.get(CSS.Property.BACKGROUND_COLOR)));
styleValues.setHorBorderSpacing(getBorderOrCellSpacing(true, css, attributes));
styleValues.setVerBorderSpacing(getBorderOrCellSpacing(false, css, attributes));
return styleValues;
}
public static TableStyleValues setBorderAttributeForCell(final Tag tag) {
TableStyleValues styleValues = new TableStyleValues();
if (tag == null) return styleValues;
Map attributes = tag.getAttributes();
Map css = tag.getCSS();
String border = attributes.get(CSS.Property.BORDER);
if (border != null && (border.trim().length() == 0 || utils.parsePxInCmMmPcToPt(attributes.get(CSS.Property.BORDER)) > 0)) {
styleValues.setBorderColor(BaseColor.BLACK);
styleValues.setBorderWidth(Table.DEFAULT_CELL_BORDER_WIDTH);
}
styleValues.setHorBorderSpacing(getBorderOrCellSpacing(true, css, attributes));
styleValues.setVerBorderSpacing(getBorderOrCellSpacing(false, css, attributes));
return styleValues;
}
/**
* Extracts and parses the style border-spacing or the attribute cellspacing
* of a table tag, if present. Favors the style border-spacing over the
* attribute cellspacing.
* If style="border-collapse:collapse" is found in the css, the spacing is
* always 0f.
* If no spacing is set, the default of 1.5pt is returned.
*
* @param getHor true for horizontal spacing, false for vertical spacing.
* @param css of the table tag.
* @param attributes of the table tag.
* @return horizontal or vertical spacing between two cells or a cell and
* the border of the table.
*/
static public float getBorderOrCellSpacing(final boolean getHor, final Map css,
final Map attributes) {
float spacing = 0f;
String collapse = css.get(CSS.Property.BORDER_COLLAPSE);
if (collapse == null || collapse.equals(CSS.Value.SEPARATE)) {
String borderSpacing = css.get(CSS.Property.BORDER_SPACING);
String cellSpacing = attributes.get(HTML.Attribute.CELLSPACING);
if (borderSpacing != null) {
if (borderSpacing.contains(" ")) {
if (getHor) {
spacing = utils.parsePxInCmMmPcToPt(borderSpacing.split(" ")[0]);
} else {
spacing = utils.parsePxInCmMmPcToPt(borderSpacing.split(" ")[1]);
}
} else {
spacing = utils.parsePxInCmMmPcToPt(borderSpacing);
}
} else if (cellSpacing != null) {
spacing = utils.parsePxInCmMmPcToPt(cellSpacing);
} else {
spacing = 2f * DEFAULT_CELL_BORDER_WIDTH;
}
} else if (collapse.equals(CSS.Value.COLLAPSE)) {
spacing = 0;
}
return spacing;
}
/**
* Sets the default cell width and widest word of a cell.
*
* - cell width = {@link Table#getCellStartWidth(HtmlCell)} + the width of
* the widest line of text.
* - widest word = {@link Table#getCellStartWidth(HtmlCell)} + the widest
* word of the cell.
*
* These 2 widths are used as the starting point when determining the width
* of the table in
*
* @param cell HtmlCell of which the widths are needed.
* @return float array containing the default cell width and the widest
* word.
*
* - float[0] = cell width.
* - float[1] = widest word.
*
*/
private float[] setCellWidthAndWidestWord(final HtmlCell cell) {
List rulesWidth = new ArrayList();
float widestWordOfCell = 0f;
float startWidth = getCellStartWidth(cell);
float cellWidth;
float widthDeviation = 0.001f;
List compositeElements = cell.getCompositeElements();
if (compositeElements != null) {
for (Element baseLevel : compositeElements) {
cellWidth = Float.NaN;
if (baseLevel instanceof Phrase) {
for (int i = 0; i < ((Phrase) baseLevel).size(); i++) {
Element inner = ((Phrase) baseLevel).get(i);
if (inner instanceof Chunk) {
if (Float.isNaN(cellWidth))
cellWidth = startWidth + widthDeviation;
cellWidth += ((Chunk) inner).getWidthPoint();
float widestWord = startWidth + widthDeviation + getCssAppliers().getChunkCssAplier().getWidestWord((Chunk) inner);
if (widestWord > widestWordOfCell) {
widestWordOfCell = widestWord;
}
}
}
if (!Float.isNaN(cellWidth))
rulesWidth.add(cellWidth);
} else if (baseLevel instanceof com.itextpdf.text.List) {
for (Element li : ((com.itextpdf.text.List) baseLevel).getItems()) {
cellWidth = startWidth + widthDeviation + ((ListItem) li).getIndentationLeft();
for (Chunk c : li.getChunks()) {
cellWidth += c.getWidthPoint();
float widestWord = getCssAppliers().getChunkCssAplier().getWidestWord(c);
if (startWidth + widthDeviation + widestWord > widestWordOfCell) {
widestWordOfCell = startWidth + widthDeviation + widestWord;
}
}
rulesWidth.add(cellWidth);
}
} else if (baseLevel instanceof PdfPTable) {
cellWidth = startWidth + widthDeviation + ((PdfPTable) baseLevel).getTotalWidth();
for (PdfPRow innerRow : ((PdfPTable) baseLevel).getRows()) {
int size = innerRow.getCells().length;
TableBorderEvent event = (TableBorderEvent) ((PdfPTable) baseLevel).getTableEvent();
TableStyleValues values = event.getTableStyleValues();
float minRowWidth = values.getBorderWidthLeft() + (size + 1) * values.getHorBorderSpacing()
+ values.getBorderWidthRight();
int celnr = 0;
for (PdfPCell innerCell : innerRow.getCells()) {
celnr++;
if (innerCell != null) {
float innerWidestWordOfCell = setCellWidthAndWidestWord(new HtmlCell(innerCell,
celnr == size))[1];
minRowWidth += innerWidestWordOfCell;
}
}
if (minRowWidth > widestWordOfCell) {
widestWordOfCell = minRowWidth;
}
}
rulesWidth.add(cellWidth);
} else if (baseLevel instanceof PdfDiv) {
PdfDiv div = (PdfDiv) baseLevel;
float divActualWidth;
if (div.getWidth() != null) {
divActualWidth = div.getWidth();
} else {
ArrayList divContent = div.getContent();
divActualWidth = calculateDivWidestElementWidth(divContent);
}
cellWidth = startWidth + widthDeviation + divActualWidth;
rulesWidth.add(cellWidth);
}
}
}
cellWidth = startWidth;
for (Float width : rulesWidth) {
if (width > cellWidth) {
cellWidth = width;
}
}
return new float[] { cellWidth, widestWordOfCell };
}
/**
* An attempt to calculate a valid div width in case it is not fixed. It is used as alternative to
* div.getActualWidth, which doesn't work here in case of not fixed div's width (it returns 0).
*
* This method is probably has to be improved in future.
*
* The main idea of this method is to return the widest element's width, so the created cell will be able to contain it.
*/
private float calculateDivWidestElementWidth(ArrayList divContent) {
float maxWidth = 0;
for (Element element : divContent) {
float width = 0;
// judging by the com.itextpdf.tool.xml.html.Div end() method, the div in XmlWorker can
// contain only paragraph, table and another div
if (element instanceof PdfDiv) {
width = calculateDivWidestElementWidth(((PdfDiv) element).getContent());
} else if (element instanceof PdfPTable) {
width = ((PdfPTable) element).getTotalWidth();
} else if (element instanceof Paragraph) {
Paragraph p = (Paragraph) element;
float widestWordOfParagraph = 0;
for (Element inner : p) {
float widestWord = 0;
if (inner instanceof Chunk) {
HashMap chunkAttributes = ((Chunk) inner).getAttributes();
if (chunkAttributes != null && chunkAttributes.containsKey(Chunk.IMAGE)) {
Object o = chunkAttributes.get(Chunk.IMAGE);
if (o instanceof Object[] && ((Object[]) o)[0] instanceof Image)
widestWord = ((Image) ((Object[]) o)[0]).getWidth();
} else {
widestWord = getCssAppliers().getChunkCssAplier().getWidestWord((Chunk) inner);
}
}
// TODO may be paragraph here could contain not only chunks?
if (widestWord > widestWordOfParagraph) {
widestWordOfParagraph = widestWord;
}
}
width = widestWordOfParagraph;
}
if (width > maxWidth) {
maxWidth = width;
}
}
return maxWidth;
}
/**
* Calculates the total width based on the given widths array and the given
* outer width.
*
* @param widths array of floats containing column width values.
* @param outerWidth equals the required space outside of the table for
* margins and borders.
* @return a table's width.
* @throws NoCustomContextException
*/
private float getTableWidth(final float[] widths, final float outerWidth) throws NoCustomContextException {
float width = 0;
for (float f : widths) {
width += f;
}
return width + outerWidth;
}
/**
* Adds horizontal values of a table and its parent if present. Following
* values are added up:
*
* - left and right margins of the table.
* - left and right border widths of the table.
* - left and right margins of the parent of the table is present.
* - one horizontal border spacing.
*
*
* @param tag
* @param horBorderSpacing
* @param ctx
* @return float containing the needed space for margins of table and
* parent(s) and the borders of the table.
* @throws NoCustomContextException
*/
private float getTableOuterWidth(final Tag tag, final float horBorderSpacing, final WorkerContext ctx)
throws NoCustomContextException {
float total = utils.getLeftAndRightMargin(tag, getHtmlPipelineContext(ctx).getPageSize().getWidth())
+ utils.checkMetricStyle(tag, CSS.Property.BORDER_LEFT_WIDTH)
+ utils.checkMetricStyle(tag, CSS.Property.BORDER_RIGHT_WIDTH)
+ horBorderSpacing;
Tag parent = tag.getParent();
if (parent != null) {
total += utils.getLeftAndRightMargin(parent, getHtmlPipelineContext(ctx).getPageSize().getWidth());
}
return total;
}
/**
* Calculates the start width of a cell. Following values are added up:
*
* - padding left, this includes left border width and a horizontal border
* spacing.
* - padding right, this includes right border width.
* - the (colspan - 1) * horizontal border spacing.
*
*
* @param cell HtmlCell of which the start width is needed.
* @return float containing the start width.
*/
private float getCellStartWidth(final HtmlCell cell) {
TableStyleValues cellStyleValues = cell.getCellValues();
// colspan - 1, because one horBorderSpacing has been added to
// paddingLeft for all cells.
int spacingMultiplier = cell.getColspan() - 1;
float spacing = spacingMultiplier * cellStyleValues.getHorBorderSpacing();
return spacing + cell.getPaddingLeft() + cell.getPaddingRight();
}
/**
* Sets the top and bottom margin of the given table.
*
* @param table PdfPTable on which the margins need to be set.
* @param t Tag containing the margin styles and font size if needed.
* @param values {@link TableStyleValues} containing border widths and
* border spacing values.
* @param ctx
* @throws NoCustomContextException
*/
private void setVerticalMargin(final PdfPTable table, final Tag t, final TableStyleValues values,
final WorkerContext ctx) throws NoCustomContextException {
float spacingBefore = values.getBorderWidthTop();
float spacingAfter = values.getVerBorderSpacing() + values.getBorderWidthBottom();
for (Entry css : t.getCSS().entrySet()) {
String key = css.getKey();
String value = css.getValue();
if (CSS.Property.MARGIN_TOP.equalsIgnoreCase(key)) {
final CssUtils utils = CssUtils.getInstance();
spacingBefore += utils.calculateMarginTop(value, fst.getFontSize(t), getHtmlPipelineContext(ctx));
} else if (CSS.Property.MARGIN_BOTTOM.equalsIgnoreCase(key)) {
float marginBottom = utils.parseValueToPt(value, fst.getFontSize(t));
spacingAfter += marginBottom;
getHtmlPipelineContext(ctx).getMemory().put(HtmlPipelineContext.LAST_MARGIN_BOTTOM, marginBottom);
} else if (CSS.Property.PADDING_TOP.equalsIgnoreCase(key)){
table.setPaddingTop(utils.parseValueToPt(value, fst.getFontSize(t)));
}
}
table.setSpacingBefore(spacingBefore);
table.setSpacingAfter(spacingAfter);
}
/*
* (non-Javadoc)
*
* @see com.itextpdf.tool.xml.TagProcessor#isStackOwner()
*/
@Override
public boolean isStackOwner() {
return true;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy