package cc.catalysts.boot.report.pdf.elements; import cc.catalysts.boot.report.pdf.config.PdfStyleSheet; import cc.catalysts.boot.report.pdf.utils.ReportAlignType; import cc.catalysts.boot.report.pdf.utils.ReportVerticalAlignType; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageContentStream; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; /** * <p><b>IMPORTANT:</b> Although this class is publicly visible, it is subject to change and may not be implemented by clients!</p> * * @author Klaus Lehner */ public class ReportTable implements ReportElement { private static boolean LAYOUTING_ASSERTIONS_ENABLED = false; private static final boolean DEFAULT_BORDER = false; private static final float DEFAULT_CELL_PADDING_LEFT_RIGHT = 2; private static final float DEFAULT_CELL_PADDING_TOP_BOTTOM = 2; private final PdfStyleSheet pdfStyleSheet; private float[] cellWidths; private ReportVerticalAlignType[] cellAligns; private ReportElement[][] elements; private ReportElement[] title; private boolean border = DEFAULT_BORDER; private boolean noBottomBorder; private boolean noTopBorder; private boolean drawInnerHorizontal = true; private boolean drawInnerVertical = true; private boolean drawOuterVertical = true; private boolean enableExtraSplitting; private boolean isSplitable = true; private Collection<ReportImage.ImagePrintIntent> intents = new LinkedList<ReportImage.ImagePrintIntent>(); /** * left and right cell padding */ private float cellPaddingX = DEFAULT_CELL_PADDING_LEFT_RIGHT; private float cellPaddingY = DEFAULT_CELL_PADDING_TOP_BOTTOM; /** * @param cellWidths width of each column (the sum of elements must be 1) * @param elements elements of each cell * @param pdfStyleSheet the stylesheet to be used for this table * @param title the titles for the report (first row) */ public ReportTable(PdfStyleSheet pdfStyleSheet, float[] cellWidths, ReportElement[][] elements, ReportElement[] title) { this.pdfStyleSheet = pdfStyleSheet; if (elements == null || cellWidths == null) { throw new IllegalArgumentException("Arguments cant be null"); } if (elements.length > 0 && cellWidths.length != elements[0].length) { throw new IllegalArgumentException("The cell widths must have the same number of elements as 'elements'"); } if (title != null && title.length != cellWidths.length) { throw new IllegalArgumentException("Title must be null, or the same size as elements"); } this.cellWidths = cellWidths; this.cellAligns = new ReportVerticalAlignType[cellWidths.length]; Arrays.fill(cellAligns, ReportVerticalAlignType.TOP); this.elements = elements; this.title = title; } public void setNoInnerBorders(boolean noInnerBorders) { this.drawInnerHorizontal = !noInnerBorders; this.drawInnerVertical = !noInnerBorders; } public void setDrawInnerVertical(boolean drawInnerVertical) { this.drawInnerVertical = drawInnerVertical; } public void setDrawInnerHorizontal(boolean drawInnerHorizontal) { this.drawInnerHorizontal = drawInnerHorizontal; } public void setNoBottomBorder(boolean border) { this.noBottomBorder = border; } public void setNoTopBorder(boolean border) { this.noTopBorder = border; } public void setDrawOuterVertical(boolean drawOuterVertical) { this.drawOuterVertical = drawOuterVertical; } public void setBorder(boolean border) { this.border = border; } public void setExtraSplitting(boolean enableExtraSplitting) { this.enableExtraSplitting = enableExtraSplitting; } /** * @param cellPaddingX for left and right */ public void setCellPaddingX(float cellPaddingX) { this.cellPaddingX = cellPaddingX; } /** * @param cellPaddingY for top and bottom */ public void setCellPaddingY(float cellPaddingY) { this.cellPaddingY = cellPaddingY; } public boolean getExtraSplitting() { return enableExtraSplitting; } @Override public float print(PDDocument document, PDPageContentStream stream, int pageNumber, float startX, float startY, float allowedWidth) throws IOException { if (title != null) { throw new IllegalStateException("title not implemented!"); } float y = startY; float previousY = y; int lineIndex = 0; for (ReportElement[] line : elements) { float calculatedHeight = LAYOUTING_ASSERTIONS_ENABLED ? getLineHeight(line, allowedWidth) : -1; y = printLine(document, stream, pageNumber, startX, y, allowedWidth, line); float actualHeight = previousY - y; if (LAYOUTING_ASSERTIONS_ENABLED && calculatedHeight != actualHeight) { throw new RuntimeException(String.format("Layout algorithm bug: layouting height calculation reported " + "different height (%s) than painting code (%s) in table with %s lines, current line index: %s", calculatedHeight, actualHeight, elements.length, lineIndex)); } boolean isFirstLine = lineIndex == 0; boolean isLastLine = lineIndex == elements.length - 1; placeBorders(stream, previousY, y, startX, allowedWidth, isFirstLine, isLastLine); previousY = y; lineIndex++; } return y; } private void placeBorders(PDPageContentStream stream, float startY, float endY, float x, float allowedWidth, boolean isFirstLine, boolean isLastLine) throws IOException { if (!border) { return; } stream.setStrokingColor(0, 0, 0); stream.setLineWidth(0.3f); float y0 = startY; float y1 = endY; float x1 = x + allowedWidth; if (drawInnerHorizontal) { if (!noBottomBorder || noBottomBorder && !isLastLine) { drawLine(stream, x, x1, y1, y1); } } // top border if (!noTopBorder && isFirstLine) { drawLine(stream, x, x1, y0, y0); } // bottom border if (!noBottomBorder && isLastLine) { drawLine(stream, x, x1, y1, y1); } float currentX = x; for (int i = 0; i < cellWidths.length; i++) { float width = cellWidths[i]; if ( (i == 0 && drawOuterVertical) || // left (i > 0 && drawInnerVertical) // inner ) { drawLine(stream, currentX, currentX, y0, y1); } currentX += width * allowedWidth; } // draw last if (drawOuterVertical) { drawLine(stream, currentX, currentX, y0, y1); } } private void drawLine(PDPageContentStream stream, float x0, float x1, float y0, float y1) throws IOException { stream.moveTo(x0, y0); stream.lineTo(x1, y1); stream.stroke(); } private float calculateVerticalAlignment(ReportElement[] line, int elementIndex, float y, float allowedWidth) { float yPos = 0; float lineHeight = getLineHeight(line, allowedWidth); switch (cellAligns[elementIndex]) { case TOP: yPos = y - cellPaddingY; break; case BOTTOM: yPos = y - cellPaddingY - lineHeight + line[elementIndex].getHeight(cellWidths[elementIndex] * allowedWidth - 2 * cellPaddingX); break; case MIDDLE: yPos = y - cellPaddingY - lineHeight / 2 + line[elementIndex].getHeight(cellWidths[elementIndex] * allowedWidth - 2 * cellPaddingX) / 2; break; default: throw new IllegalArgumentException("Vertical align type " + cellAligns[elementIndex] + " not implemented for tables"); } return yPos; } /** * draws a line. * * @return the new y position of the bottom of the line just drawn */ private float printLine(PDDocument document, PDPageContentStream stream, int pageNumber, float startX, float y, float allowedWidth, ReportElement[] line) throws IOException { float x = startX + cellPaddingX; // minY = furthest that any cell has expanded to the bottom (min since coordinate system starts at the bottom) float minY = y; for (int i = 0; i < cellWidths.length; i++) { if (line[i] != null) { float yi = 0; float yPos = calculateVerticalAlignment(line, i, y, allowedWidth); final float columnNetWidth = getAllowedNetColumnWidth(allowedWidth, i); if (line[i] instanceof ReportImage) { ReportImage reportImage = (ReportImage) line[i]; autoShrinkExcessiveImage(columnNetWidth, reportImage); yi = line[i].print(document, stream, pageNumber, x, yPos, columnNetWidth); reportImage.printImage(document, pageNumber, x, yPos); } else { yi = line[i].print(document, stream, pageNumber, x, yPos, columnNetWidth); } intents.addAll(line[i].getImageIntents()); minY = Math.min(minY, yi); } x += cellWidths[i] * allowedWidth; } return minY - cellPaddingY; } private void autoShrinkExcessiveImage(float maxWidth, ReportImage reportImage) { float initialWidth = reportImage.getWidth(); final float newHeight = reportImage.getHeight() * maxWidth / initialWidth; // only auto-shrink, don't auto-grow if (maxWidth <= reportImage.getWidth() || newHeight <= reportImage.getHeight()) { reportImage.setWidth(maxWidth); reportImage.setHeight(newHeight); } } @Override public float getHeight(float allowedWidth) { float[] maxes = new float[elements.length]; for (int lineIndex = 0; lineIndex < elements.length; lineIndex++) { maxes[lineIndex] = getLineHeight(elements[lineIndex], allowedWidth); } float max = 0; for (float f : maxes) { max += f; } return max; } @Override public boolean isSplitable() { return isSplitable; } public void setSplitable(boolean isSplitable) { this.isSplitable = isSplitable; } @Override public float getFirstSegmentHeight(float allowedWidth) { if (elements != null && elements.length > 0) { final float firstSegmentHeightFromLine = getFirstSegmentHeightFromLine(elements[0], allowedWidth); return firstSegmentHeightFromLine + pdfStyleSheet.getLineDistance(); } else { return 0; } } private float getFirstSegmentHeightFromLine(ReportElement[] line, float allowedWidth) { float maxHeight = 0f; for (int i = 0; i < line.length; i++) { if (line[i] != null) { maxHeight = Math.max(maxHeight, line[i].getFirstSegmentHeight(getAllowedNetColumnWidth(allowedWidth, i))); } } return maxHeight + 2 * cellPaddingY; } private float getLineHeight(ReportElement[] line, float allowedWidth) { float maxHeight = 0; float currentHeight; for (int columnIndex = 0; columnIndex < line.length; columnIndex++) { final float columnNetWidth = getAllowedNetColumnWidth(allowedWidth, columnIndex); if (line[columnIndex] != null) { if (line[columnIndex] instanceof ReportImage) { ReportImage reportImage = (ReportImage) line[columnIndex]; autoShrinkExcessiveImage(columnNetWidth, reportImage); currentHeight = reportImage.getHeight(); } else { currentHeight = line[columnIndex].getHeight(columnNetWidth); } maxHeight = Math.max(maxHeight, currentHeight); } } return maxHeight + 2 * cellPaddingY; } private ReportTable createNewTableWithClonedSettings(ReportElement[][] data) { ReportTable newTable = new ReportTable(pdfStyleSheet, cellWidths, data, title); newTable.setBorder(border); newTable.setCellPaddingX(cellPaddingX); newTable.setCellPaddingY(cellPaddingY); newTable.setExtraSplitting(enableExtraSplitting); return newTable; } @Override public Collection<ReportImage.ImagePrintIntent> getImageIntents() { return intents; } public ReportElement[] splitFirstCell(float allowedHeight, float allowedWidth) { ReportElement[] firstLineA = new ReportElement[elements[0].length]; ReportElement[] firstLineB = new ReportElement[elements[0].length]; boolean hasSecondPart = false; for (int i = 0; i < elements[0].length; i++) { ReportElement elem = elements[0][i]; float width = cellWidths[i] * allowedWidth - 2 * cellPaddingX; if (elem != null && elem.isSplitable()) { ReportElement[] split = elem.split(width, allowedHeight); firstLineA[i] = split[0]; firstLineB[i] = split[1]; if (firstLineB[i] != null) { hasSecondPart = true; } } else { firstLineA[i] = elem; } } if (hasSecondPart) { ReportElement[][] newMatrix = new ReportElement[elements.length][elements[0].length]; newMatrix[0] = firstLineB; for (int i = 1; i < elements.length; i++) { newMatrix[i] = elements[i]; } ReportTable firstLine = createNewTableWithClonedSettings(new ReportElement[][]{firstLineA}); ReportTable nextLines = createNewTableWithClonedSettings(newMatrix); return new ReportElement[]{firstLine, nextLines}; } else { return new ReportElement[]{this, null}; } } @Override public float getHeightOfElementToSplit(float allowedWidth, float allowedHeight) { float currentHeight = 0f; int i = 0; while (i < elements.length && (currentHeight + getLineHeight(elements[i], allowedWidth)) < allowedHeight) { currentHeight += getLineHeight(elements[i], allowedWidth); i++; } return getLineHeight(elements[i], allowedWidth); } @Override public ReportElement[] split(float allowedWidth, float allowedHeight) { float currentHeight = 0f; int lineIndex = 0; while (lineIndex < elements.length && (currentHeight + getLineHeight(elements[lineIndex], allowedWidth)) < allowedHeight) { currentHeight += getLineHeight(elements[lineIndex], allowedWidth); lineIndex++; } if (lineIndex > 0) { //they all fit until i-1, inclusive //check if the last row can be split ReportElement[][] extraRows = new ReportElement[2][elements[0].length]; boolean splittable = false; if (enableExtraSplitting) { splittable = true; for (int j = 0; j < elements[lineIndex].length; j++) { if (!elements[lineIndex][j].isSplitable() || currentHeight + elements[lineIndex][j].getFirstSegmentHeight(getAllowedNetColumnWidth(allowedWidth, j)) + 2 * cellPaddingY >= allowedHeight) { splittable = false; } } if (splittable) { for (int j = 0; j < elements[lineIndex].length; j++) { if (elements[lineIndex][j].getHeight(getAllowedNetColumnWidth(allowedWidth, j)) + currentHeight < allowedHeight) { extraRows[0][j] = elements[lineIndex][j]; extraRows[1][j] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); } else { ReportElement[] extraSplit = elements[lineIndex][j].split(getAllowedNetColumnWidth(allowedWidth, j), allowedHeight - currentHeight - 2 * cellPaddingY); extraRows[0][j] = extraSplit[0]; extraRows[1][j] = extraSplit[1]; } } } } ReportElement[][] first = new ReportElement[splittable ? lineIndex + 1 : lineIndex][elements[0].length]; ReportElement[][] next = new ReportElement[elements.length - lineIndex][elements[0].length]; for (int j = 0; j < elements.length; j++) { if (j < lineIndex) first[j] = elements[j]; else next[j - lineIndex] = elements[j]; } if (splittable) { first[lineIndex] = extraRows[0]; next[0] = extraRows[1]; } ReportTable firstLine = createNewTableWithClonedSettings(first); ReportTable nextLines = createNewTableWithClonedSettings(next); return new ReportElement[]{firstLine, nextLines}; } else { //this means first row does not fit in the given height ReportElement[][] first = new ReportElement[1][elements[0].length]; ReportElement[][] next = new ReportElement[elements.length][elements[0].length]; for (lineIndex = 1; lineIndex < elements.length; lineIndex++) next[lineIndex] = elements[lineIndex]; for (lineIndex = 0; lineIndex < elements[0].length; lineIndex++) { ReportElement[] splits = elements[0][lineIndex].split(getAllowedNetColumnWidth(allowedWidth, lineIndex), allowedHeight - 2 * cellPaddingY); if (splits[0] != null) first[0][lineIndex] = splits[0]; else first[0][lineIndex] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); if (splits[1] != null) next[0][lineIndex] = splits[1]; else next[0][lineIndex] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); } ReportTable firstLine = createNewTableWithClonedSettings(first); ReportTable nextLines = createNewTableWithClonedSettings(next); return new ReportElement[]{firstLine, nextLines}; } } /** * Gets the net column width (usable space for content, equals column width minus padding). */ private float getAllowedNetColumnWidth(float allowedTableWidth, int columnIndex) { return getAllowedGrossColumnWidth(allowedTableWidth, columnIndex) - cellPaddingX * 2; } /** * Gets the gross column width (total width of the column). */ private float getAllowedGrossColumnWidth(float allowedWidth, int columnIndex) { return cellWidths[columnIndex] * allowedWidth; } @Override public ReportElement[] split(float allowedWidth) { ReportElement[][] first = new ReportElement[][]{elements[0]}; ReportElement[][] next = Arrays.copyOfRange(elements, 1, elements.length); ReportTable firstLine = createNewTableWithClonedSettings(first); ReportTable nextLines = createNewTableWithClonedSettings(next); return new ReportElement[]{firstLine, nextLines}; } public void setTextAlignInColumn(int column, ReportAlignType alignType, boolean excludeHeader) { for (int i = excludeHeader ? 1 : 0; i < elements.length; i++) { ReportElement[] element = elements[i]; if (element[column] instanceof ReportTextBox) { ((ReportTextBox) element[column]).setAlign(alignType); } } } public void setVerticalAlignInColumn(int column, ReportVerticalAlignType alignType) { cellAligns[column] = alignType; } public ReportElement[][] getElements() { return elements; } public ReportElement[] getTitle() { return title; } public float[] getCellWidths() { return cellWidths; } public PdfStyleSheet getPdfStyleSheet() { return pdfStyleSheet; } public static void setLayoutingAssertionsEnabled(boolean enabled) { LAYOUTING_ASSERTIONS_ENABLED = enabled; } }