QuestPDF / QuestPDF

QuestPDF is a modern open-source .NET library for PDF document generation. Offering comprehensive layout engine powered by concise and discoverable C# Fluent API. Easily generate PDF reports, invoices, exports, etc.
https://www.questpdf.com
Other
11.65k stars 604 forks source link

Multiple text column unknown text length #387

Open MichelMichels opened 1 year ago

MichelMichels commented 1 year ago

Is your feature request related to a problem? Please describe. Is it at the moment possible to have multiple text columns with wrapped text? See screenshot in additional context.

Describe alternatives you've considered I could measure the length of the text, but this feels janky.

Additional context image

MarcinZiabek commented 1 year ago

Hi 😁

I am afraid that QuestPDF does not offer out-of-the-box support for this type of layout. I am planning to introduce it but it may require some serious architectural changes.

As an alternative, you can try to utilise the DynamicComponent approach but this may be slightly more difficult.

MichelMichels commented 1 year ago

No problem. I'll look into DynamicComponent.

Any idea how this could be implemented natively in QuestPDF? Some kind of flow layout? If you need help, I can offer some of my time.

MarcinZiabek commented 1 year ago

Yes, I am thinking about this topic for a couple of month already 😁

Right now, QuestPDF uses the measure-draw approach. That means, the parent can measure its children many times, with varying available space provided. And then, once everything is set, the parent can perform the draw operation. It draws the child to the canvas and also updates its state (e.g. for the column element, it updates information which items were drawn).

What is important, the draw operation can be (usually) performed only once per page. We need to be able to measure elements with various states. So, in your example, we need to perform the measure-draw operation twice, for the left and right sides of the document.

If I need to name the process, the current architecture uses 1-step probing, while we need to have many-step probing.

One of ideas that I have, is to expose elements state. So the parent can save current state of its children, then perform measurement operations, maybe change the state somehow, measure again, etc. This way, we can perform more advanced measurement operations, without actually drawing anything on the canvas. Please notice, that state consists of not only direct children but all descendants. And not all elements have state. This suddenly involves tree traversal, apaplying IDs to elements, using dictionaries to store state, etc. Quite complex stuff.

I am not sure if anything above makes sense 😅 This is a pretty rough idea, and quite difficult to describe. I have done this type of state exposing in the DynamicComponent approach, where the library saves / changes / restores state of the component many times per page drawing step, in order to best predict its size and position.

MichelMichels commented 1 year ago

I'm trying to understand but I think it's hard because I'm not familiar with the internal codebase of QuestPDF 😄. I'll look into the DynamicComponent approach and maybe that will help me understand.

Azuf commented 1 year ago

@MichelMichels have you tried the DynamicComponent approach yet? I have tried the approach of measuring the text and reducing the length till it fits in the container by using the DyamicContext.CreateElement method but, it's gotten nowhere because I have other requirements that cannot be met using this approach. Maybe you've had better luck with this?

MichelMichels commented 1 year ago

Hey @Azuf , I followed another path. I calculated the character count and made 3 columns where I put text in, but there's no automatic wrapping. It adds pages when overflowing the columns. But in my use case, this was not a problem as the content where a lot of paragraphs with titles which didn't need an order to be printed.

So I'm afraid this doesn't help you very much... Anyway, here's the code I used:

            container
                .Column(column =>
                {
                    column.Item().Row(row =>
                    {
                        row.RelativeItem()
                            .ShowOnce()
                            .Text(text =>
                            {
                                AddTitledParagraph(text, "title", content);
                                // Statically repeated code above
                            });

                        row
                            .AutoItem()
                            .PaddingHorizontal(5f, Unit.Millimetre)
                            .LineVertical(0.1f)
                            .LineColor(Colors.Grey.Lighten1);

                        row.RelativeItem()
                            .ShowOnce()
                            .Text(text =>
                            {
                                AddTitledParagraph(text, "title", content);
                                // Statically repeated code above
                            });

                        row
                            .AutoItem()
                            .PaddingHorizontal(5f, Unit.Millimetre)
                            .LineVertical(0.1f)
                            .LineColor(Colors.Grey.Lighten1);

                        row.RelativeItem()
                            .ShowOnce()
                            .Text(text =>
                            {
                                AddTitledParagraph(text, "title", content);
                                // Statically repeated code above
                            });
                    });
                });