Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A variant of "ShowEntire" that does not throw an exception. #861

Open
MercinaM opened this issue Apr 26, 2024 · 6 comments
Open

A variant of "ShowEntire" that does not throw an exception. #861

MercinaM opened this issue Apr 26, 2024 · 6 comments

Comments

@MercinaM
Copy link

MercinaM commented Apr 26, 2024

Is your feature request related to a problem? Please describe.
I'm trying to render a block of user-generated content. The content is entered by an end-user in a WYSIWYG HTML editor. I have no idea in advance what the content is, and how much space I will need to render it.

The requirement:
The aforementioned block of content should always be rendered in its entirety, unless there is no way to do so (the content cannot fit inside a single page).

The problem:
To my understanding there are currently 2 ways of solving this issue.

The first is using ShowEntire, which only works if the content can fit inside a single page. Since the content in this case is user-generated, I as the developer cannot guarantee this. Furthermore, since QuestPDF throws an error if the content does not fit, I cannot use it since it may cause the PDF generation to fail.

The second is using EnsureSpace. The problem is that it's not just the content that the end-user has control over, but they can also modify the font size and change the content inside the header and footer of the document. Consequently, even though I know the page size (A4) and the margins, I don't know of a consistent way to calculate a minHeight value that corresponds to the actual available content height of the page. If I set the value too low, then QuestPDF won't add a page break, even if it could fit the entirety of the content on a single page. If I set the value too high, then I run into the same problem as with ShowEntire.

Describe the solution you'd like
I would like to have a variant of ShowEntire that does not throw an exception if the content cannot fit on a single page, but instead simply behaves as if ShowEntire hadn't been used.

Describe alternatives you've considered
I've tried implementing a solution using a dynamic component, which to my knowledge is the only way of getting actual measurements for both the available page size and the elements I'm rendering on the page. If I could return more than a single page's content from the dynamic component's Compose method, then I would have a (passable) solution - If the content does not fit inside the first page, simply render it on the second (and beyond). But since, once again, the dynamic component throws an exception if I return more than a single page's worth of content, I run into the same problem as with ShowEntire.

@MarcinZiabek
Copy link
Member

Thank you for reaching out 😄

I am not entirely sure of what behavior you want to achieve. Would you please provide some visual examples?

@MercinaM
Copy link
Author

Examples

In all of the examples, the part of the content with the red background color is user-generated. This is the part of the content that we do not want to span multiple pages unless we have no other option.

Example 1

The content is short enough to comfortably fit inside the first page..

image

Example 2

The content is too long to fit inside the first page, but is short enough to fit inside a single page. Using ShowEntire, we can ensure the content is displayed in a single page in its entirety.

Page 1:

image

Page 2:

image

Example 3

This is where we run into a problem. The content is too long to fit into a single page. Since we're using ShowEntire to achieve the desired result for the second example, QuestPDF now throws an exception.

image

Instead of this behavior, ideally we'd like to have a method available (i.e. TryShowEntire), which would behave the same way as ShowEntire did in examples 1 and 2, but in this case, instead of throwing an exception, would simply render the content as if ShowEntire wasnt used:

Page 1:

image

Page 2:

image

Obviously I have very little idea about the inner workings of the library, and if what I'm asking for is even feasible. But if it is, it would greatly help us out.

Code used to generate the examples

private List<string> Paragraphs = new List<string>()
{
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ultricies dignissim commodo. Suspendisse cursus ultrices mi eu cursus. Nulla ultrices tortor aliquet erat faucibus tempus vel eu magna. Fusce non elit rhoncus, congue elit sed, tristique dolor. Pellentesque diam odio, luctus ac lectus id, pretium luctus eros. Ut eu blandit velit. Pellentesque at faucibus sem. Morbi ac diam id augue porta dapibus eget quis ipsum. Aenean volutpat diam in blandit egestas. Mauris sagittis semper nibh id dignissim. Nullam tempus mi ac augue dapibus, gravida laoreet nulla cursus. Aenean finibus, sapien ac aliquet molestie, dui ante tristique ante, sit amet viverra metus erat vel est. Integer ut congue dui.",

    "Praesent mattis diam eget congue convallis. Phasellus velit eros, gravida eget enim at, iaculis sollicitudin nisl. Mauris pulvinar arcu non velit vehicula, sit amet molestie elit lobortis. Nam eleifend felis quis lorem malesuada, eu feugiat urna cursus. Nulla vehicula a ante dignissim facilisis. Cras elit lacus, ornare vel suscipit at, dapibus ac nisi. Phasellus ac arcu posuere, pretium augue sed, euismod purus. Nullam lacinia, ligula convallis aliquet aliquam, odio erat dictum urna, ut mattis massa nulla vel mi. Quisque cursus nisi arcu, aliquam blandit neque euismod sed. Vestibulum elementum sapien hendrerit justo tempus pharetra. Nam ac malesuada ante. Sed eget libero arcu. Vivamus sit amet placerat dolor, vitae sagittis tortor.",

    "In vitae iaculis erat, ultricies convallis risus. Donec quis pharetra quam. Mauris semper erat in malesuada vehicula. Phasellus at nulla vulputate, posuere tellus et, pellentesque nunc. Morbi elementum dui fermentum tempus tempus. Praesent a cursus ex. Sed pellentesque maximus libero. Quisque rhoncus velit a pulvinar tempus. Integer at est non justo interdum scelerisque. Donec urna ipsum, tempus non ante eu, viverra interdum orci. Duis vitae porta libero, ac molestie sem. Nunc id arcu vel dolor tincidunt lacinia. Etiam ac diam accumsan, vulputate ante nec, scelerisque augue. Donec sed pellentesque augue, semper finibus dolor.",

    "Vivamus vel rutrum dui. Duis mattis mi ac convallis mollis. Donec a nisl et dui consequat malesuada. Aliquam a lacus odio. Vivamus et lectus in elit mattis porta. Donec a suscipit est. Donec blandit, ligula nec bibendum tempor, metus mi mollis eros, varius rutrum justo arcu in neque. Morbi varius molestie massa, in mollis ipsum volutpat eu. Nunc venenatis enim sit amet eros gravida, eget venenatis turpis commodo. In molestie vitae mauris ac congue. Cras facilisis, metus vitae consequat fringilla, lacus felis scelerisque ligula, quis auctor nisl nulla quis velit. Duis maximus, nisi id egestas feugiat, leo elit aliquam nisl, efficitur sagittis ipsum lectus cursus libero. Mauris at finibus sem.",

    "Nullam fermentum rutrum elementum. Nullam porttitor, dolor a dapibus auctor, quam dolor facilisis nunc, a pulvinar nisi lectus vel diam. Curabitur tempor, massa vel blandit consequat, turpis urna blandit justo, quis mattis justo libero nec velit. Cras imperdiet nulla eu nisl tincidunt, a euismod ligula pellentesque. Proin iaculis mi at orci finibus, non sagittis ante ornare. Nullam vitae risus id sem ornare malesuada eget ut odio. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis sit amet urna vitae nisi hendrerit imperdiet. Sed id est sapien. Pellentesque in odio ut nunc interdum dictum. Phasellus eu elementum lacus, et interdum ipsum. Praesent nec lacus congue lacus cursus euismod nec vitae massa."
};

public void Compose(IDocumentContainer container)
{
    container.Page(page =>
    {
        page.Margin(10f, Unit.Millimetre);
        page.DefaultTextStyle(TextStyle
            .Default
            .FontFamily(Fonts.SegoeUI)
            .FontSize(12f)
            .LineHeight(1.2f)
            .NormalWeight());

        page.Header()
            .Background("#FFFF00")
            .DefaultTextStyle(style => style.FontSize(48f))
            .Text("Page Header");
        
        page.Content()
            .Column(column =>
            {
                column.Item()
                    .Background("#00FFFF")
                    .PaddingVertical(5f, Unit.Millimetre)
                    .Text(text =>
                    {
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Line("This part is not user generated");
                        text.Span("This part is not user generated");
                    });

                column.Item()
                    .Background("#FF0000")
                    .ShowEntire()
                    .PaddingVertical(5f, Unit.Millimetre)
                    .Text(string.Join('\n', Paragraphs.Take(5)));

                column.Item()
                    .Background("#00FFFF")
                    .PaddingVertical(5f, Unit.Millimetre)
                    .Text("This part is not user generated");

                column.Item()
                    .Background("#00FFFF")
                    .Table(table =>
                    {
                        table.ColumnsDefinition(columns =>
                        {
                            columns.RelativeColumn(2f);
                            columns.RelativeColumn(5f);
                            columns.RelativeColumn(3f);
                            columns.RelativeColumn(2f);
                        });

                        table.Header(header =>
                        {
                            header.Cell().Text("Column 1").Bold();
                            header.Cell().Text("Column 2").Bold();
                            header.Cell().Text("Column 3").Bold();
                            header.Cell().Text("Column 4").Bold();
                        });

                        for (var i = 0; i < 30; i++)
                        {
                            table.Cell().Text(i.ToString());
                            table.Cell().Text(i.ToString());
                            table.Cell().Text(i.ToString());
                            table.Cell().Text(i.ToString());
                        }
                    });
            });

        page.Footer()
            .Background("#FFFF00")
            .DefaultTextStyle(style => style.FontSize(9f))
                .Text("Page Footer");
    });
}

@MarcinZiabek
Copy link
Member

My suggestion is to use the EnsureSpace with a value slightly smaller than the content height (page height - header - footer). If I am not mistaken, it may produce the desired outcome.

@MercinaM
Copy link
Author

MercinaM commented May 6, 2024

Well, as I mentioned in the original post, I don't know in advance how large the the header or footer are going to be. For a simplified example of our use case, lets presume the user can modify the following properties:

public struct HeaderSettings {
    public HeaderType Type { get; set; } // Text or Image
    public string Text { get; set; } // Used when header type is "Text".
    public float FontSize { get; set; } // Used when header type is "Text".
    public byte[] Image { get; set; } // Used when header type is "Image".
}
public struct FooterSettings {
    public FooterType Type { get; set; } // Text or Image
    public string Text { get; set; } // Used when footer type is "Text".
    public float FontSize { get; set; } // Used when footer type is "Text".
    public byte[] Image { get; set; } // Used when footer type is "Image".
}

I have no idea how big the header or footer are going to be. That also means there is no easy way for me to safely use EnsureSpace.

The only way I could think of to safely use EnsureSpace is to essentially do a dry run, where I only render the header and footer, and use a dynamic component that renders nothing and just measures the available space on the page. If I then take the calculated height value (and perhaps subtract a millimetre or so), it should probably be safe to use in EnsureSpace.

@MarcinZiabek
Copy link
Member

I wonder... we could create a variant of EnsureSpace that attempts to use all available vertical space as its argument.

I already have trouble with naming: ShowEntire, EnsureSpace... and how that third option could be named?

@Prototipo-Erick-Santander

I think this is something like a feature I would also need for my current project, I need to show a table, and I need the table to be shown in its entirety unless absolutely impossible, I don't know how many rows the table will have, and the rows can have different heights. Using ShowEntire mostly works but can throw stray exceptions once in a while, I think that a 'safe' version of the ShowEntire, something like a TryShowEntire() that would try to fit the content on the current page, if that is not enough, tries to fit it in 1 page, if it still does not fit, it just breaks the content to 2 or more pages.

As @MercinaM said I was about to implement this using Dynamic Component, but a more native way to do this would be greatly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants