PDF 中带有水平分页符的表格

Posted

技术标签:

【中文标题】PDF 中带有水平分页符的表格【英文标题】:Tables in PDF with horizontal page breaks 【发布时间】:2013-02-27 23:02:00 【问题描述】:

有人知道 Java 的(最好是开源的)PDF 布局引擎,能够呈现带有水平分页符的表格吗? “水平分页”至少是该功能在 BIRT 中的命名方式,但要澄清一下:如果表格有太多列无法适应可用页面宽度,我希望表格在多个页面中水平拆分,例如对于 10 列的表,第 1-4 列输出在第一页,第 5-10 列在第二页输出。当然,如果表格的太多而无法垂直放置在一页上,这当然也应该在后面的页面上重复。

到目前为止,搜索产品非常困难。我认为这样的功能在其他产品中可能会有不同的命名,因此很难使用阿姨谷歌找到合适的解决方案。

到目前为止,我已经尝试过:

BIRT 声称支持这一点,但实际实现有很多错误,以至于无法使用。我认为这样的功能是不言而喻的,行高在所有页面中保持一致,从而可以在将页面彼此相邻放置时对齐行。然而,BIRT 会为每个页面单独计算所需的行高。

Jasper 没有支持。

我也考虑过 Apache FOP,但我在 XSL-FO 规范中找不到任何合适的语法。

对于这个任务,iText 通常有点太“低级”了(使得对预期 PDF 文档的其他部分进行布局变得困难),但似乎不提供支持。

李>

由于似乎有几十个其他报告或布局引擎可能适合也可能不适合,而且我发现要准确猜测要查找的内容有点困难,我希望有人可能已经有类似的要求并且可以至少提供一个正确方向的建议。产品可以很容易地集成到 Java 服务器应用程序中是相对重要的,本地 Java 库将是理想的。

现在,要保持所有页面的行对齐,必须按如下方式计算行高:

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

虽然 BIRT 目前似乎在做类似的事情:

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)

【问题讨论】:

你看过dynamicjasper.com吗? 您的列宽是否高度动态/可变?我的意思是你知道是一两列加宽导致水平中断要求还是它可能是任何一列?还是您的列数不定? @Dave Jarvis:不,但是如果 Jasper 不能布置这样的表格,我不明白 DynamicJasper 为什么应该这样做?还是我错过了什么?就像一个通知:我不需要有人在这里向我指出任意报告引擎,我可以自己谷歌。 @jowierun:列数是固定的,固定列宽也是可以接受的。 你能通过多张表来解决这个问题吗?例如 table1 有前 5 列,table2 有后 5 列? 【参考方案1】:

可以使用iText 以您想要的方式显示表格。您需要使用自定义表格定位和自定义行列写入。

我能够调整this iText example 以水平和垂直地在多个页面上书写。这个想法是记住在页面上垂直进入的开始行和结束行。我已经把整个代码都放好了,这样你就可以轻松运行它了。

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);
    

我知道iText 对于报告而言可能级别太低,但它可以与标准报告工具一起使用,以满足此类特殊需求。

更新:现在首先拆分高于页面高度的行。如果行高 2、3、...、n 倍,则代码不会进行拆分,但也可以对此进行调整。

【讨论】:

您如何解决我在对原始问题的评论中提到的问题:writeSelectedRows 显然只能将整行写入一页。如果单行太高而无法放在一页上,如何在该单行内应用分页符? 是的,我忽略了这个问题。我认为一个想法是拆分高行以适合当前页面并在下一个上呈现新的剩余行。我稍后会尝试更新答案。 @jarnbjo 好的,更新了代码,现在在渲染前分割高行。 这实际上似乎可以解决问题。使用 iText 布局 PDF 确实非常乏味,但我们已经在使用 iText 将多个来源的多个 PDF 连接成一个 PDF。如果我将其包装在一个稍微更直观的 API 中,我们可以使用此代码至少生成有问题的表格,然后将表格包含在完整文档中其他来源的片段之间的正确位置。非常感谢! @dcernahoschi 有什么想法可以在使用 itext(最新版本)从 html 生成 pdf 时停止拆分表吗?【参考方案2】:

这里的想法与 Dev Blanked 相同,但使用 wkhtmltopdf (https://code.google.com/p/wkhtmltopdf/) 和一些 javascript,您可以实现您所需要的。当针对fiddle 运行 wkhtmltopdf 时,您会得到如下所示的结果(pdf 页面的屏幕截图)。您可以将“break-after”类放在标题行的任何位置。我们在 Java EE Web 应用程序中使用 wkhtmltopdf 服务器端来生成动态报告,性能实际上非常好。

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>

脚本

$(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);      
    );
);

【讨论】:

其实不是一个坏主意,不是因为 wkhtmltopdf 弄乱了大部分其他布局。错误跟踪器中有超过 600 个未解决的错误,最后一个版本是 18 个月大。即使渲染一个简单的文本行也会失败,如下图所示:jarnbjo.de/wkhtml2pdf.png - 第一行是预期的输出,在第二行(wkhtmltopdf 输出)中,字体大小和字母间距都不正确。 @jarnbo:同意将 HTML 渲染为 PDF 需要一些格式,因为两者不在同一个坐标空间中播放。我们创建为 PDF 打印(特殊字体、图表)定制的特殊标准化 HTML 页面(带有 SVG 图表)。尽管存在未解决的问题,但我们在与多个客户进行的大量测试中从未遇到过崩溃或故障。还要记住,生成的 PDF 渲染取决于您的 PDF 阅读器,例如:Helvetica 字体在 Linux 上不可用,这会使屏幕上的渲染与预期不同。 在 CSS 中使用物理测量单位(pt、cm、英寸等),我希望在创建的 PDF 中相应地采用这些单位。显然他们不是。我只是浏览了一些开放的错误,不幸的是,其中许多错误阻止我们使用该工具,例如分页错误导致在文本行中间插入分页符,导致上半部分呈现到当前页面,下半部分呈现在下一行。这些错误实际上是在 Webkit 打印引擎中,而不是在 wkhtml2pdf 工具中,但这并不能解决问题。 你是对的:根据一些布局算法,有一个坐标空间修改。也许您最终会基于 iText 或其他一些 PDF 库编写自己的布局引擎。如果您找到任何解决方案与我们分享,那就太好了【参考方案3】:

你试过http://code.google.com/p/flying-saucer/。它应该将 HTML 转换为 PDF。

【讨论】:

不,我没有。而且由于恕我直言,HTML 和 CSS 中没有标记功能或样式属性来描述所需的布局,我不知道 Flying Saucer 将如何提供帮助。如果你知道怎么做,请告诉。 您可以使用普通的 HTML 表格并将列和行的宽度/高度设置为适当的 px 值。如果您可以使用 JSP 在网页中获得所需的结构,则可能飞碟可以生成所需的 pdf。 能否详细说明。对我来说,设置列宽如何强制分页符对我来说并不明显,除非 Flying Saucer 实现了一些额外的 CSS 魔法,根据我的测试它没有。如果我只是指定列宽,加起来超过了页面宽度,飞碟要么缩小列以使其适合一页,要么如果单元格内容太宽,最后一个适合的列会在右侧页面边框处被裁剪.【参考方案4】:

我的建议是使用 FOP 变压器。

Here你可以看到一些例子以及如何使用它。

Here你可以找到一些关于 fop 和表格的例子。

【讨论】:

我已经写过为什么我没有尝试过 Apache FOP。如果您知道如何使用 XSL-FO 实现所需的布局,那就足够了,但是您链接的示例与我的实际问题无关。 +1 用于 Apache FOP。我用它来生成丰富的 pdf 报告,具有复杂的网格和大量的图像(当然需要将某些元素强烈地保持在一页上,等等)。 Apache FOP 不是一个糟糕的解决方案,如果它的功能满足您的要求,您不会受到任何错误的影响并且可以忍受相当差的性能。除非我可以使用 FOP 创建我特别要求的布局,否则它不能回答我的问题。【参考方案5】:

Jasper 没有支持。

根据 Jasper 文档,它确实有支持,通过:

分栏元素(即具有 type=column 属性的分栏元素)。这可以放置在报告中的任何位置。 组/标题上的 isStartNewColumn 属性

见http://books.google.com.au/books?id=LWTbssKt6MUC&pg=PA165&lpg=PA165&dq=jasper+reports+%22column+break%22&source=bl&ots=aSKZfqgHR5&sig=KlH4_OiLP-cNsBPGJ7yzWPYgH_k&hl=en&sa=X&ei=h_1kUb6YO6uhiAeNk4GYCw&redir_esc=y#v=onepage&q=column%20break&f=false

如果你真的卡住了,作为最后的手段,你可以使用 Excel / OpenOffice Calc:手动将数据复制到单元格中,根据需要手动格式化,另存为 xls 格式。然后使用 java 中的 apache POI 动态填充/替换所需的数据并打印到文件/PDF。至少它提供了对列和行格式/中断/边距等的非常细粒度的控制。

【讨论】:

您链接的搜索结果中提到的分栏似乎都是指文本列而不是表格列。如果我遗漏了什么,请澄清。使用 Excel 或 OpenOffice 是可能的,但我们需要远程控制其中任何一个产品。不幸的是,服务器策略不允许安装此类软件,即使我们这样做了,远程处理 MS Office 和 Open Office 也很麻烦(去过那里,做过)。

以上是关于PDF 中带有水平分页符的表格的主要内容,如果未能解决你的问题,请参考以下文章

允许其接受分页符的 NETSuite PDF 模板都有哪些要求?

打印带有强制分页符的表格

请问word中分页符怎么弄

替代 PDF 用于动态生成的带有分页符的文档

Chrome (v51) - 用于在表格行之间插入分页符的媒体查询 - 仅限横向

WORD中怎样设置分页符