Draw custom borders for table with more flexibilit

2020-02-06 18:43发布

问题:

Previously I asked a question about drawing custom table borders: Draw custom borders for table spanning across more than one page in itext7

And there an answer was provided with a way to draw custom borders, but it does not allow to affect the complete row or column line (for example to tilt it slightly) because cell borders are drawn separately for each cell.

I want to add some randomness to table borders like in the following screenshot:

Here is the code I have that works for tables that fit into a single page, but if table is spanned across several pages the code does not work anymore:

PdfDocument pdfDoc = new PdfDocument(new PdfWriter(dest));
        Document doc = new Document(pdfDoc);
        doc.add(new Paragraph("Table event"));
        Table table = new Table(UnitValue.createPercentArray(3)).useAllAvailableWidth();
        table.setNextRenderer(new DottedLineTableRenderer(table, new Table.RowRange(0, 0)));
        String s ="";
        for(int i=0;i<35;i++){
        s+="\nTest";
         }
        table.addCell(new Cell().add(new Paragraph(s)).setBorder(Border.NO_BORDER));
        table.addCell(new Cell().add(new Paragraph("A2")).setBorder(Border.NO_BORDER));
        table.addCell(new Cell().add(new Paragraph("A3")).setBorder(Border.NO_BORDER));


        doc.add(table);


        doc.close();

private class DottedLineTableRenderer extends TableRenderer {
    public DottedLineTableRenderer(Table modelElement, Table.RowRange rowRange) {
        super(modelElement, rowRange);
    }

    @Override
    public void drawChildren(DrawContext drawContext) {
        super.drawChildren(drawContext);
        PdfCanvas canvas = drawContext.getCanvas();
        int maxLineTo = 5;
        int minLineTo = 2;
        int lineToHorizontalLine = (int)(Math.random() * maxLineTo) + minLineTo;

        int maxSkewHor = 5;
        int minSkewHor = 2;
        int skewHorizontalLine = (int)(Math.random() * maxSkewHor) + minSkewHor;

        int maxVerticalLine = 5;
        int minVerticalLine = 2;
        int lineToVerticalLine = (int)(Math.random() * maxVerticalLine) + minVerticalLine;


        canvas.setLineWidth(2).setStrokeColor(new DeviceRgb(222, 27, 27));

        // first horizontal line
        CellRenderer[] cellRenderers = rows.get(0);
        canvas.moveTo(cellRenderers[0].getOccupiedArea().getBBox().getLeft()-lineToHorizontalLine,
                cellRenderers[0].getOccupiedArea().getBBox().getTop());
        canvas.lineTo(cellRenderers[cellRenderers.length - 1].getOccupiedArea().getBBox().getRight()+lineToHorizontalLine,
                cellRenderers[cellRenderers.length - 1].getOccupiedArea().getBBox().getTop());


        for (int i = 0; i < rows.size(); i++) {
            skewHorizontalLine = (int)(Math.random() * maxSkewHor) + minSkewHor;
            lineToHorizontalLine = (int)(Math.random() * maxLineTo) + minLineTo;
            cellRenderers = rows.get(i);
            // horizontal lines
            canvas.moveTo(cellRenderers[0].getOccupiedArea().getBBox().getX()-lineToHorizontalLine,
                    cellRenderers[0].getOccupiedArea().getBBox().getY()+skewHorizontalLine);
            canvas.lineTo(cellRenderers[cellRenderers.length - 1].getOccupiedArea().getBBox().getRight()+lineToHorizontalLine,
                    cellRenderers[cellRenderers.length - 1].getOccupiedArea().getBBox().getBottom());

            // first vertical line
            Rectangle cellRect = cellRenderers[0].getOccupiedArea().getBBox();
            canvas.moveTo(cellRect.getLeft(), cellRect.getBottom());

            canvas.lineTo(cellRect.getLeft(), cellRect.getTop()+lineToVerticalLine );

            // vertical lines
            for (int j = 0; j < cellRenderers.length; j++) {
                lineToVerticalLine = (int)(Math.random() * maxVerticalLine) + minVerticalLine;


                cellRect = cellRenderers[j].getOccupiedArea().getBBox();
                canvas.moveTo(cellRect.getRight(), cellRect.getBottom()-lineToVerticalLine);
                canvas.lineTo(cellRect.getRight(), cellRect.getTop()+lineToVerticalLine); //ячейки

            }
        }
        canvas.stroke();
    }
}

I want to draw custom borders by myself :)

回答1:

First of all, we need to override the renderer correctly, i.e. override getNextRenderer() method. Currently TableRenderer is quite problematic to override because the parameterless constructor of TableRenderer is not accessible, and the other constructors do some implicit work that changes the state. But we can still work around this problem with the following code:

@Override
public IRenderer getNextRenderer() {
    CustomTableRenderer nextTable = new CustomTableRenderer((Table) modelElement);
    nextTable.rows.clear();
    nextTable.rowRange = null;
    return nextTable;
}

Disclaimer: since the answer uses private implementation details of TableRenderer it may not work in the future. It works with 7.1.6 which is the latest released version at the moment of writing this post. You should create a custom fork of the code for such purposes. Pull requests are welcome as well.

If we look at the implementation of TableRenderer we see that the class contains heights (line in code) and countedColumnWidth (line in code) fields which sound interesting, but they are private. It means that we should create our custom fork of iText (source code available at https://github.com/itext/itext7), make those fields protected and use them in our subclass.

You may use reflection in your code instead at your own risk, but it should not be used because it may not work with your JVM (changing accessibility modifiers is strongly discouraged) or may not work in the next version of iText, or may not work for another reason. I will not add reflection code in my answer to further discourage its usage.

All we need to do it override drawBorders() method. Here is the code that already adds some randomness to the lines.

private static class CustomTableRenderer extends TableRenderer {
    public CustomTableRenderer(Table modelElement) {
        super(modelElement);
    }

    @Override
    public IRenderer getNextRenderer() {
        CustomTableRenderer nextTable = new CustomTableRenderer((Table) modelElement);
        nextTable.rows.clear();
        nextTable.rowRange = null;
        return nextTable;
    }

    @Override
    protected void drawBorders(DrawContext drawContext) {
        PdfCanvas canvas = drawContext.getCanvas();
        canvas.saveState();
        canvas.setStrokeColor(ColorConstants.RED);

        Random r = new Random();

        // Draw vertical lines
        float curX = getOccupiedAreaBBox().getLeft();
        for (int i = 0; i <= countedColumnWidth.length; i++) {
            canvas.moveTo(curX, getOccupiedAreaBBox().getTop() + 3);
            canvas.lineTo(curX + r.nextInt(4), getOccupiedAreaBBox().getBottom() - 3);
            if (i < countedColumnWidth.length) {
                float curWidth = countedColumnWidth[i];
                curX += curWidth;
            }
        }

        // Draw horizontal lines
        float curY = getOccupiedAreaBBox().getBottom();
        for (int i = 0; i <= heights.size(); i++) {
            canvas.moveTo(getOccupiedAreaBBox().getLeft() - 3, curY);
            canvas.lineTo(getOccupiedAreaBBox().getRight() + 3, curY + r.nextInt(4));
            if (i < heights.size()) {
                float curHeight = heights.get(i);
                curY += curHeight;
            }
        }

        canvas.stroke();
        canvas.restoreState();
    }
}

To activate the custom renderer, set it to the table just before adding to the document:

PdfDocument pdfDoc = new PdfDocument(new PdfWriter(outFileName));
Document doc = new Document(pdfDoc);

Table table = new Table(UnitValue.createPercentArray(new float[]{30, 30}));
for (int i = 0; i < 40; i++) {
    table.addCell(new Cell().add(new Paragraph("Hello")));
    table.addCell(new Cell().add(new Paragraph("World")));
    table.startNewRow();
}
table.setNextRenderer(new CustomTableRenderer(table));
doc.add(table);

This is how the resultant table looks like: