vandeseer / easytable

Small table drawing library built upon Apache PDFBox
MIT License
246 stars 94 forks source link

Cells spanning multiple pages do not render properly #107

Open Thejipppp opened 3 years ago

Thejipppp commented 3 years ago

Hi

This is basically part two of the problem described in #106, and I think it's double with #96, but I wanted to be sure by adding a little image for a minimal example. (Another time, thanks for fixing #106 VERY quickly, you're amazing)

It is immediately clear that the first gray cell should span over the second page and the borders shouldn't disappear in the image below.

What I wanted to do is split those cells by checking if the height is higher than a single page. The problem is that you can't set the rowspan of a cell that is builded (which is reasonable). Basically I have a whole machinery to make List< Row >s but to be able to adapt the rowspan, I would need to convert the whole machinery to work with List<List< AbstractCellBuilder >>s which is a bit clumsy I think. But I guess there is no different option? πŸ˜‹

Kind regards Jesper

image


Table table = Table.builder().addColumnsOfWidth(200, 200).borderColor(BLACK).fontSize(8)
                .font(HELVETICA_BOLD_OBLIQUE)
                .addRow(Row.builder().backgroundColor(GRAY).textColor(BLACK).horizontalAlignment(CENTER)
                        .add(TextCell.builder().borderWidth(1).text("Markup").build())
                        .add(TextCell.builder().borderWidth(1).text("No Markup").build()).build())
                .addRow(Row.builder().backgroundColor(LIGHT_GRAY).add(ParagraphCell.builder().borderWidth(1).padding(8)
                        .lineSpacing(1.2f).rowSpan(34)
                        .paragraph(Paragraph.builder().append(Markup.builder()
                                .markup("Beneath is an example of __underscored__ and __{0.25:}striked through__ \n"
                                        + " __ __{0.25:}text, both at the same time__ __")
                                .font(Markup.MarkupSupportedFont.TIMES).build()).build())
                        .build())
                        .add(ParagraphCell.builder().borderWidth(1).padding(8).lineSpacing(1.2f).paragraph(Paragraph
                                .builder()
                                .append(StyledText
                                        .builder().text("This is some text in one font.").font(HELVETICA).build())
                                .appendNewLine()
                                .append(StyledText.builder().text(
                                        "But this text that introduces a link that follows is different. Here comes the link: ")
                                        .font(COURIER_BOLD).fontSize(6f).build())
                                .append(Hyperlink.builder().text("github!").url("http://www.github.com")
                                        .font(COURIER_BOLD).fontSize(6f).color(WHITE).build())
                                .appendNewLine(6f)
                                .append(StyledText.builder().text(
                                        "There was the link. And now we are using the default font from the cell.")
                                        .build())
                                .build()).build())
                        .build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build()).build();
        return table;
vandeseer commented 3 years ago

Hi Jesper,

using row span and extremely large cells together doesn't really work properly. Reason being that easytable was never designed for this use case: Its main goal was to allow for simple table creation (like the ones in the examples) where one could use some two or three cells bundled together by column and/or row spanning.

To be honest introducing row span was way harder than I thought (think of the edge cases like page wrapping, combination with column spanning or even both). When trying to use easytable as a "layout engine" – it gets nasty :smile: It was simply not made for the purpose.

Also it's not entirely clear how it should behave in the case where you have row spanning and page wrapping: Should it split the text automatically and continue on the next page? But if so, where and how should it split (you need to mind hyphens, word breaking etc.)? This is pretty tricky stuff and a bit too much for a voluntarily maintained open source project I don't even use myself that much anymore :wink:

Well, still I would like to help you solving your issue. My approach would be to create some "helper cells" like so:

private Table createParagraphTable() {

    Table.TableBuilder tableBuilder = Table.builder().addColumnsOfWidth(200, 200).borderColor(BLACK).fontSize(8)
            .font(HELVETICA_BOLD_OBLIQUE)
            .addRow(Row.builder().backgroundColor(GRAY).textColor(BLACK).horizontalAlignment(CENTER)
                    .add(TextCell.builder().borderWidth(1).text("Markup").build())
                    .add(TextCell.builder().borderWidth(1).text("No Markup").build()).build())
            .addRow(Row.builder().backgroundColor(LIGHT_GRAY).add(ParagraphCell.builder().borderWidth(1).padding(8)
                    .lineSpacing(1.2f).rowSpan(4)
                    .paragraph(Paragraph.builder().append(Markup.builder()
                            .markup("Beneath is an example of __underscored__ and __{0.25:}striked through__ \n"
                                    + " __ __{0.25:}text, both at the same time__ __")
                            .font(Markup.MarkupSupportedFont.TIMES).build()).build())
                    .build())
                    .add(ParagraphCell.builder().borderWidth(1).padding(8).lineSpacing(1.2f).paragraph(Paragraph
                            .builder()
                            .append(StyledText
                                    .builder().text("This is some text in one font.").font(HELVETICA).build())
                            .appendNewLine()
                            .append(StyledText.builder().text(
                                    "But this text that introduces a link that follows is different. Here comes the link: ")
                                    .font(COURIER_BOLD).fontSize(6f).build())
                            .append(Hyperlink.builder().text("github!").url("http://www.github.com")
                                    .font(COURIER_BOLD).fontSize(6f).color(WHITE).build())
                            .appendNewLine(6f)
                            .append(StyledText.builder().text(
                                    "There was the link. And now we are using the default font from the cell.")
                                    .build())
                            .build()).build())
                    .build())
            .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
            .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
            .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build());

    for (int i = 0; i <10; i++) {
        tableBuilder
                .addRow(
                        Row.builder()
                                .add(TextCell.builder()
                                        .backgroundColor(LIGHT_GRAY)
                                        .borderWidthLeft(1)
                                        .borderWidthRight(1)
                                        .lineSpacing(1.2f)
                                        .rowSpan(4)
                                        .text("Helper cell")
                                        .build()
                                )
                                .add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build())
                        .build()
                )
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build())
                .addRow(Row.builder().add(TextCell.builder().text("AAA\nAAAAAAA\nAAAAAA").build()).build());
        i++;
    }

    return tableBuilder.build();
}

This way you could feed some small pieces into the right side while still having the visual appearance of a cell that spans over multiple rows (even if it does not in practice). That'd be my approach but I don't know your exact requirements neither the context around it (like how much influence you have on the data that is fed in e.g.).

Let me know if this helps, Stefan

PS: Of course I am always open for pull requests, donations or beer in order to improve easytable :wink:

Thejipppp commented 3 years ago

Hi Stefan

I completely understand your point. You have a nice library and I am asking too much of it. (I think that happens for every open source project after a while) πŸ˜‹

In my case, I use an image at the top of the first row, so I'll do something like "If it can be drawn, draw it, else put it on the next page". It will be a struggle to be sure, but anyway, without easytable, it would be completely impossible to do it in a clean way πŸ˜„

Using helpercells won't probably do what I want, but thanks for the suggestion, I also considered itπŸ˜‡

When I find some time, I'll see which one of your three suggestions would be best πŸ˜‹

Thanks for the quick replies and keep up the amazing work! Jesper

Thejipppp commented 3 years ago

Hi

I looked through the code and I can see why it's a problem πŸ˜„

I think the solution would be to

  1. copy the Table when setting it in the TableDrawer
  2. check every cell that spans multiple rows
  3. try to split it into parts by making an empty copy of it.
  4. checking the height of all pages for every action

But the longer I look at it, the more complicated it gets.

So I agree, it would be too much work to fix that, and I'm really happy for your quick fix on the paragraph cells, so I can make a big workaround πŸ˜‹

If someone else would have this problem, what I did was rewrote my hole machinery

Maybe if I am really bored sometime, I'll try to implement it here, generally, but it's a bit too much at the moment...