Size, position, and rendering of containers

Content is the most important part of a document. There is no doubt about it. Another crucial part is the formatting that creates clear, professional, and effective communication. A properly formatted document is visually appealing, readable, and easy to navigate through.

You might already know how to organize content using containers. And how to apply a background color to them. This article describes how to specify size and position for containers. It also covers advanced capabilities like conditional rendering of content. There is information about support for right-to-left content directions.

Containers positioning

The LayoutContainer class provides everything you need to arrange your containers professionally. By applying padding and alignment, you can create a document that leaves a positive impression on its users.

This article is part of a series about Layout API for PDF generation. If you are new to the API, read the Getting Started with Layout API part first.

Docotic.Pdf library 9.5.17548-dev Layout add-on 9.5.17548-dev
Regression tests 14,726 passed Total NuGet downloads 4,514,921

Size

By default, containers occupy the smallest area required for their content. To put it differently, the size of the container is equal to the intrinsic size of its content.

The intrinsic size of an image determined by the dimensions of the image file itself. The intrinsic size of a text span is the size of the area that covers all the glyphs in the span.

The size of a compound container like the Column or the Table depends on the size of the container parts.

Width & Height

It is possible to specify an exact width and height of a container by using the Width and the Height methods. This is very convenient for placeholder containers.

An exact size also works well for images. This is because Layout API scales them to fit or fill the container depending on their ImageContentMode.

You should be careful with exact sizes for compound containers and containers with text. You will get a LayoutException when it's not possible to fit a content in the provided size.

There are cases when you only want to set a constraint on width or height. You can set constraints by using the MinWidth, the MinHeight, the MaxWidth, and the MaxHeight methods.

Please note that the library will throw a LayoutException when it's not possible to satisfy the constraints.

Extend

A container can extend itself to take maximum available space. This comes in handy when you don't know an exact size or size constraints.

Use the ExtendHorizontal method when you want the container to take all available space in the horizontal direction only. The ExtendVertical method is useful when you want to extend the container in the vertical direction only. The Extend method causes the container to take all available space in both directions.

var gray = new PdfGrayColor(75);
var text = "Content goes here";
var size = new PdfSize(150, 50);

PdfDocumentBuilder.Create().Generate("positioning-extend.pdf", doc =>
{
    for (int i = 0; i < 4; i++)
    {
        doc.Pages(page =>
        {
            page.Size(size);
            page.Content().Row(r =>
            {
                var container = r.AutoItem().Background(gray);
                switch (i)
                {
                    case 0:
                        container.Text(text);
                        break;

                    case 1:
                        container.ExtendHorizontal().Text(text);
                        break;

                    case 2:
                        container.ExtendVertical().Text(text);
                        break;

                    case 3:
                        container.Extend().Text(text);
                        break;
                }
            });
        });
    }
});

The result of the above code is in positioning-extend.pdf. As you can see, each of the four pages contains the same text on a gray background. But the size of the container is different on each page.

MinimalBox

The LayoutContainer class provides the MinimalBox method. It is kind of opposite to Extend methods. The MinimalBox method produces nested containers that only use minimal necessary space for the content.

var gray = new PdfGrayColor(75);
var size = new PdfSize(150, 50);

PdfDocumentBuilder.Create().Generate("positioning-minimalbox.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(size);
        page.Content().MinimalBox().Background(gray).Text("I don't want more space");
    });

    doc.Pages(page =>
    {
        page.Size(size);
        page.Content().Background(gray).Text("I'll take everything");
    });
});

The result of the above code is in positioning-minimalbox.pdf. Because of the MinimalBox call, the text on the first page occupies only required space. On the second page, the text covers the entire page.

Scale

It is possible to scale any content in a container. The Scale method affects content in both horizontal and vertical directions. Use the ScaleHorizontal or the ScaleVertical method to change content in one direction only. The last two methods do not preserve the aspect ratio of the content.

Scale values less than 1 reduce the area occupied by a container. Values larger than 1 increase the area. To flip content in a container, use a negative scale value. For example, ScaleVertical(-1) results in an upside-down version of the original content.

PdfDocumentBuilder.Create().Generate("positioning-scale.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content()
            .MinimalBox()
            .Column(column =>
            {
                var scales = new[] { 0.5f, 0.75f, 1, 1.3f, 1.5f };

                foreach (var scale in scales)
                {
                    var percent = (int)(scale * 100);
                    column.Item()
                        .Scale(scale)
                        .Text(FormattableString.Invariant($"Scale equals {scale} ({percent}%)."))
                            .FontSize(20);

                    column.Item().LineHorizontal(0.5);
                }
            });
    });
});

The result of the above code is in positioning-scale.pdf.

ScaleToFit

You can scale content down to fit in an available space. For example, when you have a fixed area to output a person's name or address. Of course, you can increase the area in case of a longer name. But an easier approach might be to scale the text down a bit. Use the ScaleToFit method to scale the content down.

The ScaleToFit preserves the aspect ratio of the content. The method never makes content larger. Please note that this method performs iterative calculations. This might slow down the PDF generation process.

PdfDocumentBuilder.Create().Generate("positioning-scaletofit.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().Column(column =>
        {
            for (int i = 0; i < 5; i++)
            {
                column.Item()
                    .Width(230 - 20 * i)
                    .Height(20)
                    .ScaleToFit()
                    .Border(b => b.Thickness(0.5))
                    .Text(" This text should fit into the changing width.");
            }
        });
    });
});

The result of the above code is in positioning-scaletofit.pdf.

AspectRatio

Aspect ratio defines the proportional relationship between a container's width and height. To find out the aspect ratio of a container, divide its width by its height.

Use the AspectRatio method to specify the aspect ratio for a container. It helps when you design a reusable container for different layouts or page sizes.

PdfDocumentBuilder.Create().Generate("positioning-aspectratio.pdf", doc =>
{
    var ratios = new double[] { 0.25, 0.5, 1, 2 };
    foreach (var ratio in ratios)
    {
        var ratioText = ratio.ToString(CultureInfo.InvariantCulture);
        doc.Pages(page =>
        {
            page.Size(200, 200);
            page.Content().Column(column =>
            {
                column.Item()
                    .AspectRatio(ratio)
                    .Background(new PdfGrayColor(75))
                    .Text($"Width / Heigth = {ratioText}");
            });
        });
    }
});

The result of the above code is in positioning-aspectratio.pdf.

The method has the optional parameter of type AspectRatioMode. Use this parameter to specify how to resize content while preserving the aspect ratio.

A container with a specified aspect ratio takes as much space as possible. Depending on the mode, the container will try to occupy the entire available area (default), width, or height.

Please note that the library can throw a LayoutException. It happens when it can not satisfy size, aspect ratio, and aspect ratio mode requirements.

Unconstrained

Containers can have no size constraints at all. Use the Unconstrained method to remove all size constraints from a container.

Content in an unconstrained container occupies space with size equal to the content's intrinsic size. The unconstrained container itself occupies no space. As the result, sibling containers can cover the content of the unconstrained container.

PdfDocumentBuilder.Create().Generate("positioning-unconstrained.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().MinimalBox()
            .Border(b => b.Thickness(0.5))
            .Column(column =>
            {
                column.Item().Text("First item");

                column.Item().Unconstrained()
                    .Text("Second item ignores all size constraints");

                // using an empty line for the third item
                column.Item().Text(new string(' ', 20))
                    .BackgroundColor(new PdfRgbColor(187, 237, 237), 50);

                column.Item().Text("Fourth item");
            });
    });
});

The result of the above code is in positioning-unconstrained.pdf. In the code, I used a line of spaces with a semi-transparent background for the third item in the column. As you can see, the third item partially covers the second (unconstrained) item.

Position

Position of a container depends on a number of things. Some of them are alignment, padding, position of the parent container, and content direction. By default, any container sticks to the leftmost and topmost available position.

Padding

One of the most common requirements is to add some space around container's content. The LayoutContainer class provides a set of methods to set up padding area of a container. The padding area is the space between its content and its border. To put it differently, padding represents the inner space surrounding the content.

The Padding method sets the padding on all four sides of a container at once. Use the PaddingHorizontal method to specify padding on only left and right sides. The PaddingVertical does the same on top and bottom sides only. To set up padding individually on a side, use one of the PaddingTop/Bottom/Left/Right methods.

Align

To change position of a container, use alignment methods. The AlignLeft/AlignCenter/AlignRight methods apply a horizontal alignment and return a nested container. The AlignTop/AlignMiddle/AlignBottom methods return a nested container with a corresponding vertical alignment.

A container with explicitly applied alignment takes the area with the minimum required width and/or height. The following code creates a column with two items. One item has explicitly applied alignment.

PdfDocumentBuilder.Create().Generate("positioning-alignment.pdf", doc =>
{
    var color = new PdfRgbColor(187, 237, 237);
    var text = "Hello";
    doc.Pages(page =>
    {
        page.Size(200, 100);
        page.Content().Column(c =>
        {
            c.Item().Extend().Background(color).Text(text);
            c.Item().Extend().AlignLeft().Background(color).Text(text);
        });
    });
});

The Extend call makes both items occupy the whole page. I call the AlignLeft method on the second item. This call does not change the position because the item's container aligns the text to the left by default. But the explicitly applied alignment changes the area occupied by the second item.

The result of the above code is in positioning-alignment.pdf.

Translate

To reposition a container horizontally and/or vertically, use the Translate/TranslateX/TranslateY methods. The first one moves containers both horizontally and vertically. The other two move containers in one direction only.

All these methods override position but preserve size constraints. The shifted container can overlap with other containers. Use negative parameter values to move to the left and/or to the top. Positive values cause a shift to the right and/or to the bottom.

PdfDocumentBuilder.Create().Generate("positioning-translate.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(200, 100);
        page.Content().Row(r =>
        {
            r.ConstantItem(50)
                .Background(new PdfRgbColor(187, 237, 237))
                .Text("Left");

            r.ConstantItem(50)
                // moving this item 10 points left and 5 points lower
                .Translate(-10, 5)
                .Background(new PdfRgbColor(15, 130, 9))
                .Text("Right");
        });
    });
});

The result of the above code is in positioning-translate.pdf.

Rotate

Rotated content, especially text, can improve your documents in different ways. For example, you can conserve space and make your document more engaging and aesthetically pleasing.

The LayoutContainer class provides two approaches to rotate content. No matter which approach you use, the container with the rotated content respects position and size constraints.

Rotate by 90 degrees

The RotateRight and RotateLeft methods rotate content by 90 degrees clockwise and counter-clockwise, respectively.

The following code shows how to create a document with vertical text next to the main page content.

PdfDocumentBuilder.Create().Generate("positioning-rotate.pdf", doc =>
{
    var lightGray = new PdfGrayColor(90);
    doc.Pages(page =>
    {
        page.Size(298, 210);
        page.Content().Row(r =>
        {
            r.AutoItem()
                .RotateLeft()
                .Background(lightGray)
                .Text("This content goes up");

            r.RelativeItem(1)
                .ExtendVertical()
                .PaddingHorizontal(10)
                .Column(t =>
                {
                    for (int i = 0; i < 15; i++)
                        t.Item().Text("The main content line goes here");
                });

            r.AutoItem()
                .RotateRight()
                .Background(lightGray)
                .Text("This content goes down");
        });
    });
});

The result of the above code is in positioning-rotate.pdf.

Rotate to any angle

The Rotate method rotates content by an arbitrary number of degrees. Positive numbers cause clockwise rotation. Negative numbers are for counterclockwise rotation.

Rotation origin point is the top left corner of the container. The rotated content can overlap with other containers.

PdfDocumentBuilder.Create().Generate("positioning-rotate2.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(298, 210);
        page.Content()
            .Padding(25)
            .Background(new PdfGrayColor(70)) // gray
            .AlignCenter()
            .AlignMiddle()

            .Background(new PdfGrayColor(100)) // white

            .Rotate(30)

            .Width(100)
            .Height(100)
            .Background(new PdfRgbColor(187, 237, 237)); // blue
    });
});

The result of the above code is in positioning-rotate2.pdf.

To change the rotation origin point, call one of the Translate methods before the Rotate call. Don't forget to translate the origin back after the call.

// translate the rotation origin point
.TranslateX(50)
.TranslateY(50)

.Rotate(30)

// translate back
.TranslateX(-50)
.TranslateY(-50)

Conditional layout

The layout of your PDF document can depend on a condition. For example, you can use different alignment or background color for even and odd rows in a column.

Use the Container method to insert a nested container with a layout that depends on a condition. Calling this method won't break the chain of calls.

PdfDocumentBuilder.Create().Generate("positioning-container.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().Column(c =>
        {
            for (int i = 0; i < 15; i++)
            {
                c.Item()
                    .TextStyle(TextStyle.Parent.FontSize(14))
                    .Container(x => i % 2 == 0 ? x.Background(new PdfGrayColor(70)) : x)
                    .Text($"Row {i + 1}");
            }
        });
    });
});

The result of the above code is in positioning-container.pdf.

DSL

Some parts of a document can use the same layout. For example, they can set the same-looking border or use the same formatting. According to "don't repeat yourself" principle, I recommend extracting common code into a method.

Using an extension method for the common code provides two benefits:

  • You can use the method in chains of method calls
  • You can have a meaningful name for a set of method calls

Given these benefits, you can construct a domain-specific language (DSL). With the DSL, your layout code can be shorter and easier to understand.

static class LayoutHelpers
{
    public static LayoutContainer NumberCell(this Table table)
        => table.Cell().Border(b => b.Thickness(0.5)).PaddingHorizontal(10);
}

PdfDocumentBuilder.Create().Generate("positioning-dsl.pdf", doc => doc.Pages(page =>
{
    page.Content().Table(t =>
    {
        t.Columns(c =>
        {
            for (int i = 0; i < 4; ++i)
                c.ConstantColumn(50);
        });

        for (int i = 0; i < 16; i++)
            t.NumberCell().Text($"{i + 1}");
    });
}));

Rendering process

Layout add-on arranges any content you put in a container according to size and position constraints. Some content can span multiple pages. There is the strict order in which content is rendered. Content flow is another name for this order.

The LayoutContainer class provides some methods for tweaking the content flow. You might not need them every time, but there are cases when there is no other way to achieve the required layout.

PageBreak

When you need to start a block of content from a new page, use the PageBreak method. For example, you can use the method to start a Column item from a new page.

Here is a sample code that divides a column so that each page contains only two rows.

PdfDocumentBuilder.Create().Generate("positioning-pagebreak.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Column(c =>
    {
        for (int i = 1; i <= 10; ++i)
        {
            c.Item().Text($"Item {i}");

            if (i % 2 == 0)
                c.Item().PageBreak();
        }
    });
}));

The result of the above code is in positioning-pagebreak.pdf.

ShowIf

Depending on a condition, you might need to show/hide a container. There is the ShowIf method that essentially is a syntactic sugar for this special case of conditional layout.

In the following code, I use the ShowIf method to insert vertical line after each 5 elements in a row.

PdfDocumentBuilder.Create().Generate("positioning-showif.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Row(r =>
    {
        for (int i = 0; i < 10; ++i)
        {
            r.AutoItem().Text(i.ToString());
            r.AutoItem().ShowIf(i > 0 && (i + 1) % 5 == 0).LineVertical(0.5);
        }
    });
}));

The result of the above code is in positioning-showif.pdf.

ShowOnce

You can prevent a piece of content from repeating on the next pages. Use the ShowOnce method to instruct the layout engine to fully render the content one time only.

Check the following code to see how the ShowOnce call prevents "Environment" from appearing on the second page.

PdfDocumentBuilder.Create().Generate("positioning-showonce.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Row(r =>
    {
        r.RelativeItem()
            .Background(new PdfGrayColor(75))
            .Border(b => b.Thickness(0.5))
            .Padding(5)
            .ShowOnce()
            .Text("Environment");

        r.RelativeItem()
            .Border(b => b.Thickness(0.5))
            .Padding(5)
            .Column(c =>
            {
                c.Item().Text(Environment.OSVersion.VersionString);
                c.Item().Text(string.Empty);
                c.Item().Text(
                    Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")
                    ?? string.Empty
                );
            });
    });
}));

The result of the above code is in positioning-showonce.pdf.

ShowEntire

The default behaviour is to split content between pages when it does not fit on one page. Use the ShowEntire method to render the entire container on one page.

Please note that the library throws a LayoutException when it's not possible to fit the entire content on one page.

Due to the ShowEntire call in the following code, the second item text starts from the second page. Without the call, it would start on the first page, immediately after the first item text.

PdfDocumentBuilder.Create().Generate("positioning-showentire.pdf", doc => doc.Pages(page =>
{
    page.Size(100, 100);
    page.Content().Column(c =>
    {
        c.Item().Text(t =>
        {
            for (var i = 0; i < 4; i++)
                t.Line($"First item line {i + 1}");
        });

        c.Item()
            .Background(new PdfRgbColor(250, 123, 5))
            .ShowEntire()
            .Text(t =>
            {
                for (var i = 0; i < 4; i++)
                    t.Line($"Second item line {i + 1}");
            });
    });
}));

The result of the above code is in positioning-showentire.pdf.

EnsureSpace

In a sense, the EnsureSpace is a special case of the ShowEntire method. The difference is that the EnsureSpace does not require entire content to fit on a page. The method only tries to fit a part of the content with the specified height. Everything else will go on the next page.

If an unoccupied area of the current page has a height less than the requested, the entire content will be rendered on a new page. Here, the method will produce the same result as the ShowEntire method.

StopPaging

Use the StopPaging method to produce output on at most one page. Calling this method on a container prevents its paging. Layout add-on will not split this container's content between pages. It will not render any data that won't fit on one page.

The following sample code adds two sets of pages to the document. Both sets use a list of weekday names as their content. First set has one page only, because the code calls the StopPaging method for the pages' content container.

PdfDocumentBuilder.Create().Generate("positioning-stoppaging.pdf", doc =>
{
    static Action<TextContainer> produceText(string heading)
    {
        var text = string.Join('\n', DateTimeFormatInfo.InvariantInfo.DayNames);
        return t =>
        {
            t.Line(heading).BackgroundColor(new PdfRgbColor(250, 123, 5));
            t.Span(text);
        };
    }

    doc.Pages(page =>
    {
        page.Size(100, 100);
        page.Content()
            .StopPaging()
            .Text(produceText("Without paging:"));
    });

    doc.Pages(page =>
    {
        page.Size(100, 100);
        page.Content()
            .Text(produceText("Default behaviour:"));
    });
});

The result of the above code is in positioning-stoppaging.pdf.

SkipOnce

You can postpone content appearing. If a container appears on more than one page, use the SkipOnce to skip the first page and render the content on all pages starting from the second page.

This ability is useful for all kinds of headers, but you can use it with other content too. Check the following code to see how the SkipOnce call prevents the header from appearing on the first page.

PdfDocumentBuilder.Create().Generate("positioning-skiponce.pdf", doc => doc.Pages(page =>
{
    page.Size(298, 210);

    page.Header()
        .SkipOnce()
        .Text("This header will appear starting from page 2")
        .Style(TextStyle.Parent.Underline());

    page.Content().Column(c =>
    {
        for (int i = 0; i < 5; i++)
        {
            if (i > 0)
                c.Item().PageBreak();

            c.Item().Text($"Page {i + 1}");
        }
    });
}));

The result of the above code is in positioning-skiponce.pdf.

Content direction

The default content direction is left-to-right. Containers align their text and other content to the left.

But there are languages written from right to left (e.g. Arabic and Hebrew). When creating content in these languages, use the ContentFromRightToLeft method. A call to it will switch container's content direction to right-to-left. The method will also switch default alignment.

If most of the content on your pages is in an RTL language, you can set the right-to-left as the default content direction for the pages. Use the PageLayout.ContentFromRightToLeft method for this. Then, to override the default content direction for selected containers, use the ContentFromLeftToRight method.

Please note that content direction does not affect explicitly specified alignment. E.g., right-aligned content will stick to the right side no matter what content direction you set for its container. The visual order of child elements will be different depending on the direction of the content.