Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tables in PDF with horizontal page breaks

Does someone know a (preferably open-source) PDF layout engine for Java, capable of rendering tables with horizontal page breaks? "Horizontal page breaking" is at least how the feature is named in BIRT, but to clarify: If a table has too many columns to fit across the available page width, I want the table to be split horizontally across multiple pages, e.g. for a 10-column table, the columns 1-4 to be output on the first page and columns 5-10 on the second page. This should of course also be repeated on the following pages, if the table has too many rows to fit vertically on one page.

So far, it has been quite difficult to search for products. I reckon that such a feature may be named differently in other products, making it difficult to use aunt Google to find a suitable solution.

So far, I've tried:

  • BIRT claims to support this, but the actual implementation is so buggy, that it cannot be used. I though it is self-evident for such a functionality, that the row height is kept consistent across all pages, making it possible to align the rows when placing the pages next to each other. BIRT however calculates the required row height separately for each page.

  • Jasper has no support.

  • I also considered Apache FOP, but I don't find any suitable syntax for this in the XSL-FO specification.

  • iText is generally a little bit too "low level" for this task anyway (making it difficult to layout other parts of the intended PDF documents), but does not seem to offer support.

Since there seem to be some dozens other reporting or layout engines, which may or may not fit and I find it a little bit difficult to guess exactly what to look for, I was hoping that someone perhaps already had similar requirements and can provide at least a suggestion in the right direction. It is relatively important that the product can be easily integrated in a Java server application, a native Java library would be ideal.

Expected Layout

Now, to keep the rows aligned across all pages, the row heights must be calculated as follows:

Row1.height = max(A1.height, B1.height, C1.height, D1.height)
Row2.height = max(A2.height, B2.height, C2.height, D2.height)

While BIRT currently seem to do something like:

Page1.Row1.height = max(A1.height, B1.height)
Page2.Row1.height = max(C1.height, D1.height)
Page1.Row2.height = max(A2.height, B2.height)
Page2.Row2.height = max(C2.height, D2.height)

Second Layout

like image 382
jarnbjo Avatar asked Mar 12 '13 12:03

jarnbjo


People also ask

How do I fit a table in PDF?

Select the cells or table you want to embed, and then choose Edit > Cut or Copy. Place the insertion point in the cell where you want the table to appear, and then choose Edit > Paste. Click inside a cell, choose Table > Insert Table, specify the number of rows and columns, and then click OK.

What is a PDF table?

Tables are part of many PDF forms. Tables are commonly set up. with columns and rows having a header at the top that describes the content for each column and two or more rows of data following the header. You find tables in a variety of forms.


2 Answers

It's possible to display a table the way you want with iText. You need to use custom table positioning and custom row and column writing.

I was able to adapt this iText example to write on multiple pages horizontally and vertically. The idea is to remember the start and end row that get in vertically on a page. I've put the whole code so you can easily run it.

public class Main {
    public static final String RESULT = "results/part1/chapter04/zhang.pdf";

    public static final float PAGE_HEIGHT = PageSize.A4.getHeight() - 100f;

    public void createPdf(String filename)
            throws IOException, DocumentException {

        // step 1
        Document document = new Document();
        // step 2
        PdfWriter writer
                = PdfWriter.getInstance(document, new FileOutputStream(filename));
        // step 3
        document.open();

        //setup of the table: first row is a really tall one
        PdfPTable table = new PdfPTable(new float[] {1, 5, 5, 1});

        StringBuilder sb = new StringBuilder();

        for(int i = 0; i < 50; i++) {
            sb.append("tall text").append(i + 1).append("\n");
        }

        for(int i = 0; i < 4; i++) {
            table.addCell(sb.toString());
        }

        for (int i = 0; i < 50; i++) {
            sb = new StringBuilder("some text");
            table.addCell(sb.append(i + 1).append(" col1").toString());

            sb = new StringBuilder("some text");
            table.addCell(sb.append(i + 1).append(" col2").toString());

            sb = new StringBuilder("some text");
            table.addCell(sb.append(i + 1).append(" col3").toString());

            sb = new StringBuilder("some text");
            table.addCell(sb.append(i + 1).append(" col4").toString());
        }

        // set the total width of the table
        table.setTotalWidth(600);
        PdfContentByte canvas = writer.getDirectContent();

        ArrayList<PdfPRow> rows = table.getRows();

        //check every row height and split it if is taller than the page height
        //can be enhanced to split if the row is 2,3, ... n times higher than the page  
        for (int i = 0; i < rows.size(); i++) {
            PdfPRow currentRow = rows.get(i);

            float rowHeight = currentRow.getMaxHeights();

            if(rowHeight > PAGE_HEIGHT) {
                PdfPRow newRow = currentRow.splitRow(table,i, PAGE_HEIGHT);
                if(newRow != null) {
                    rows.add(++i, newRow);
                }
            }
        }

        List<Integer[]> chunks = new ArrayList<Integer[]>();

        int startRow = 0;
        int endRow = 0;
        float chunkHeight = 0;

        //determine how many rows gets in one page vertically
        //and remember the first and last row that gets in one page
        for (int i = 0; i < rows.size(); i++) {
            PdfPRow currentRow = rows.get(i);

            chunkHeight += currentRow.getMaxHeights();

            endRow = i;   

            //verify against some desired height
            if (chunkHeight > PAGE_HEIGHT) {
                //remember start and end row
                chunks.add(new Integer[]{startRow, endRow});
                startRow = endRow;
                chunkHeight = 0;
                i--;
            }
        }

        //last pair
        chunks.add(new Integer[]{startRow, endRow + 1});

        //render each pair of startRow - endRow on 2 pages horizontally, get to the next page for the next pair
        for(Integer[] chunk : chunks) {
            table.writeSelectedRows(0, 2, chunk[0], chunk[1], 236, 806, canvas);
            document.newPage();
            table.writeSelectedRows(2, -1, chunk[0], chunk[1], 36, 806, canvas);

            document.newPage();
        }


        document.close();
    }

    public static void main(String[] args) throws IOException, DocumentException {
        new Main().createPdf(RESULT);
    }
}

I understand that maybe iText is too low level just for reports, but it can be employed beside standard reporting tools for special needs like this.

Update: Now rows taller than page height are first splited. The code doesn't do splitting if the row is 2, 3,..., n times taller but can be adapted for this too.

like image 84
dcernahoschi Avatar answered Oct 24 '22 20:10

dcernahoschi


Same idea here than Dev Blanked but using wkhtmltopdf (https://code.google.com/p/wkhtmltopdf/) and some javascript, you can achieve what you need. When running wkhtmltopdf against this fiddle you get the result shown below (screenshot of pdf pages). You can place the "break-after" class anywhere on the header row. We use wkhtmltopdf server-side in a Java EE web app to produce dynamic reports and the performance is actually very good.

HTML

<body>
        <table id="table">
            <thead>
                <tr><th >Header 1</th><th class="break-after">Header 2</th><th>Header 3</th><th>Header 4</th></tr>
            </thead>
            <tbody>
                <tr valign="top">
                    <td>A1<br/>text<br/>text</td>
                    <td>B1<br/>text</td>
                    <td>C1</td>
                    <td>D1</td>
                </tr>
                <tr valign="top">
                    <td>A2</td>
                    <td>B2<br/>text<br/>text<br/>text</td>
                    <td>C2</td>
                    <td>D2<br/>text</td>
                </tr>
            </tbody>
        </table>
    </body>

Script

$(document).ready(function() {
    var thisTable = $('#table'),
        otherTable= thisTable.clone(false, true),
        breakAfterIndex = $('tr th', thisTable).index($('tr th.break-after', thisTable)),
        wrapper = $('<div/>');

    wrapper.css({'page-break-before': 'always'});
    wrapper.append(otherTable);
    thisTable.after(wrapper);
    $('tr', thisTable).find('th:gt(' + breakAfterIndex + ')').remove(); 
    $('tr', thisTable).find('td:gt(' + breakAfterIndex + ')').remove(); 
    $('tr', otherTable).find('th:lt(' + (breakAfterIndex + 1) + ')').remove(); 
    $('tr', otherTable).find('td:lt(' + (breakAfterIndex + 1) + ')').remove();

    $('tr', table).each(function(index) {
        var $this =$(this),
            $otherTr = $($('tr', otherTable).get(index)),
            maxHeight = Math.max($this.height(), $otherTr.height());
        $this.height(maxHeight);
        $otherTr.height(maxHeight);      
    });
});

Screenshot of the resulting PDF

like image 1
Guy Avatar answered Oct 24 '22 19:10

Guy