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 ReportTableWithVerticalLines implements ReportElement { 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 = 0; private static final int BORDER_Y_DELTA = 1; 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 noInnerBorders = false; private boolean noHorizontalBorders = false; private boolean placeFirstBorder = true; private boolean placeLastBorder = 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; /** * Copy Constructur, that creates a new ReportTable on basis of the provided Table * @param table ReportTable that is copied to this object */ public ReportTableWithVerticalLines(ReportTable table) { this.pdfStyleSheet = table.getPdfStyleSheet(); if (table.getElements() == null || table.getCellWidths() == null) { throw new IllegalArgumentException("Arguments cant be null"); } if (table.getElements().length > 0 && table.getCellWidths().length != table.getElements()[0].length) { throw new IllegalArgumentException("The cell widths must have the same number of elements as 'elements'"); } if (table.getTitle() != null && table.getTitle().length != table.getCellWidths().length) { throw new IllegalArgumentException("Title must be null, or the same size as elements"); } this.cellWidths = table.getCellWidths(); this.cellAligns = new ReportVerticalAlignType[table.getCellWidths().length]; Arrays.fill(cellAligns, ReportVerticalAlignType.TOP); this.elements = table.getElements(); this.title = table.getTitle(); } public ReportTableWithVerticalLines(ReportTableWithVerticalLines table, ReportElement[][] data) { this.pdfStyleSheet = table.getPdfStyleSheet(); if (table.getElements() == null || table.getCellWidths() == null) { throw new IllegalArgumentException("Arguments cant be null"); } if (table.getElements().length > 0 && table.getCellWidths().length != table.getElements()[0].length) { throw new IllegalArgumentException("The cell widths must have the same number of elements as 'elements'"); } if (table.getTitle() != null && table.getTitle().length != table.getCellWidths().length) { throw new IllegalArgumentException("Title must be null, or the same size as elements"); } this.cellWidths = table.getCellWidths(); this.cellAligns = new ReportVerticalAlignType[table.getCellWidths().length]; Arrays.fill(cellAligns, ReportVerticalAlignType.TOP); this.elements = data; this.title = table.getTitle(); } public void setNoInnerBorders(boolean noInnerBorders) { this.noInnerBorders = noInnerBorders; } public void setNoBottomBorder(boolean border) { this.noBottomBorder = border; } public void setNoHorizontalBorders(boolean noHorizontalBorders) { this.noHorizontalBorders = noHorizontalBorders; } public void setNoTopBorder(boolean border) { this.noTopBorder = border; } 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; int i = 0; float lineY = 0; for (ReportElement[] line : elements) { float lineHeight = getLineHeight(line, allowedWidth) + pdfStyleSheet.getLineDistance(); y = printLine(document, stream, pageNumber, startX, y, allowedWidth, line, lineY); placeFirstBorder = i == 0; placeLastBorder = i == elements.length - 1; placeBorders(stream, startY, y, startX, allowedWidth); i++; lineY += lineHeight; } return y; } private void placeBorders(PDPageContentStream stream, float startY, float endY, float x, float allowedWidth) throws IOException { if (border) { stream.setStrokingColor(0, 0, 0); stream.setLineWidth(0.3f); float y0 = startY - BORDER_Y_DELTA; float y1 = endY - (BORDER_Y_DELTA + 1); if (!noInnerBorders) { if (!noTopBorder || noTopBorder && !placeFirstBorder) { stream.moveTo(x, y0); stream.lineTo(x + allowedWidth, y0); stream.stroke(); } if (!noBottomBorder || noBottomBorder && !placeLastBorder) { stream.moveTo(x, y1); stream.lineTo(x + allowedWidth, y1); stream.stroke(); } } else { if (!noTopBorder && placeFirstBorder) { stream.moveTo(x, y0); stream.lineTo(x + allowedWidth, y0); stream.stroke(); } if (!noBottomBorder && placeLastBorder) { stream.moveTo(x, y1); stream.lineTo(x + allowedWidth, y1); stream.stroke(); } } if(!noHorizontalBorders) { float currX = x; stream.moveTo(currX, y0); stream.lineTo(currX, y1); stream.stroke(); for (float width : cellWidths) { if (!noInnerBorders) { stream.moveTo(currX, y0); stream.lineTo(currX, y1); stream.stroke(); } currX += width * allowedWidth; } stream.moveTo(currX, y0); stream.lineTo(currX, 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; } private float printLine(PDDocument document, PDPageContentStream stream, int pageNumber, float startX, float y, float allowedWidth, ReportElement[] line, float previousLineHeight) throws IOException { float x = startX + cellPaddingX; 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); if (line[i] instanceof ReportImage) { ReportImage reportImage = (ReportImage) line[i]; float initialWidth = reportImage.getWidth(); reportImage.setWidth(cellWidths[i] * allowedWidth - cellPaddingX * 2); reportImage.setHeight(reportImage.getHeight() * (cellWidths[i] * allowedWidth - cellPaddingX * 2) / initialWidth); yi = line[i].print(document, stream, pageNumber, x, yPos, cellWidths[i] * allowedWidth - cellPaddingX * 2); reportImage.printImage(document, pageNumber, x, yPos); } else { yi = line[i].print(document, stream, pageNumber, x, yPos, cellWidths[i] * allowedWidth - cellPaddingX * 2); } intents.addAll(line[i].getImageIntents()); minY = Math.min(minY, yi); } x += cellWidths[i] * allowedWidth; } return minY - cellPaddingY; } @Override public float getHeight(float allowedWidth) { float[] maxes = new float[elements.length]; for (int i = 0; i < elements.length; i++) { maxes[i] = getLineHeight(elements[i], 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) { return border ? getFirstSegmentHeightFromLine(elements[0], allowedWidth) + BORDER_Y_DELTA : getFirstSegmentHeightFromLine(elements[0], allowedWidth); } 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(cellWidths[i] * allowedWidth - cellPaddingX * 2)); } return maxHeight + 2 * cellPaddingY; } private float getLineHeight(ReportElement[] line, float allowedWidth) { float maxHeight = 0; float currentHeight; for (int i = 0; i < line.length; i++) { if (line[i] != null) { if (line[i] instanceof ReportImage) { ReportImage lineImage = (ReportImage) line[i]; currentHeight = lineImage.getHeight() * (cellWidths[i] * allowedWidth - cellPaddingX * 2) / lineImage.getWidth(); } else { currentHeight = line[i].getHeight(cellWidths[i] * allowedWidth - cellPaddingX * 2); } maxHeight = Math.max(maxHeight, currentHeight); } } return maxHeight + 2 * cellPaddingY; } private ReportTableWithVerticalLines createNewTableWithClonedSettings(ReportElement[][] data) { ReportTableWithVerticalLines newTable = new ReportTableWithVerticalLines(this, data); newTable.setBorder(border); newTable.setNoBottomBorder(noBottomBorder); newTable.setNoHorizontalBorders(noHorizontalBorders); newTable.setNoInnerBorders(noInnerBorders); newTable.setNoTopBorder(noTopBorder); 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]; } ReportTableWithVerticalLines firstLine = createNewTableWithClonedSettings(new ReportElement[][]{firstLineA}); ReportTableWithVerticalLines 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 i = 0; while (i < elements.length && (currentHeight + getLineHeight(elements[i], allowedWidth)) < allowedHeight) { currentHeight += getLineHeight(elements[i], allowedWidth); i++; } if (i > 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[i].length; j++) { if (!elements[i][j].isSplitable() || currentHeight + elements[i][j].getFirstSegmentHeight(cellWidths[j] * allowedWidth - cellPaddingX * 2) + 2 * cellPaddingY >= allowedHeight) { splittable = false; } } if (splittable) { for (int j = 0; j < elements[i].length; j++) { if (elements[i][j].getHeight(cellWidths[j] * allowedWidth - cellPaddingX * 2) + currentHeight < allowedHeight) { extraRows[0][j] = elements[i][j]; extraRows[1][j] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); } else { ReportElement[] extraSplit = elements[i][j].split(cellWidths[j] * allowedWidth - cellPaddingX * 2, allowedHeight - currentHeight - 2 * cellPaddingY); extraRows[0][j] = extraSplit[0]; extraRows[1][j] = extraSplit[1]; } } } } ReportElement[][] first = new ReportElement[splittable ? i + 1 : i][elements[0].length]; ReportElement[][] next = new ReportElement[elements.length - i][elements[0].length]; for (int j = 0; j < elements.length; j++) { if (j < i) first[j] = elements[j]; else next[j - i] = elements[j]; } if (splittable) { first[i] = extraRows[0]; next[0] = extraRows[1]; } ReportTableWithVerticalLines firstLine = createNewTableWithClonedSettings(first); ReportTableWithVerticalLines 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 (i = 1; i < elements.length; i++) next[i] = elements[i]; for (i = 0; i < elements[0].length; i++) { ReportElement[] splits = elements[0][i].split(cellWidths[i] * allowedWidth - cellPaddingX * 2, allowedHeight - 2 * cellPaddingY); if (splits[0] != null) first[0][i] = splits[0]; else first[0][i] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); if (splits[1] != null) next[0][i] = splits[1]; else next[0][i] = new ReportTextBox(pdfStyleSheet.getBodyText(), pdfStyleSheet.getLineDistance(), ""); } ReportTableWithVerticalLines firstLine = createNewTableWithClonedSettings(first); ReportTableWithVerticalLines nextLines = createNewTableWithClonedSettings(next); return new ReportElement[]{firstLine, nextLines}; } } @Override public ReportElement[] split(float allowedWidth) { ReportElement[][] first = new ReportElement[][]{elements[0]}; ReportElement[][] next = Arrays.copyOfRange(elements, 1, elements.length); ReportTableWithVerticalLines firstLine = createNewTableWithClonedSettings(first); ReportTableWithVerticalLines 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; } }