Эта страница может содержать автоматически переведенный текст.

Составные контейнеры

Составные контейнеры играют важную роль в структурировании и организации содержимого. С помощью правильного контейнера можно легко и удобно представлять текст и изображения.

Безусловно, документ можно построить, используя только простые контейнеры для текста и изображений. Однако существуют требования, которые сложно или невозможно реализовать только с помощью простых контейнеров. В таких случаях помогают составные контейнеры. Кроме того, сложные контейнеры помогают достичь цели с меньшим объемом кода.

Составные контейнеры

Используйте методы класса LayoutContainer, чтобы добавлять на страницы документа составные контейнеры, такие как Row и Column. С помощью этих контейнеров можно реализовать и другие контейнеры, например сетку и списки. Также есть методы Inlined и Layers для менее распространенных, но не менее важных случаев.

Эта статья является частью серии о Layout API для генерации PDF. Если вы впервые знакомитесь с API, сначала прочитайте раздел Начало работы с Layout API.

Row

Контейнеры Row предназначены для элементов, расположенных горизонтально в одну линию. Каждый элемент в строке является контейнером. Это означает, что в одной строке можно размещать содержимое разных типов. Интервал между элементами можно задавать с помощью метода Spacing.

У всех элементов в строке одинаковая высота. Библиотека использует высоту самого высокого элемента как высоту строки. Есть три способа задать ширину элемента. Вы должны выбрать один из них при создании элемента. В одной строке могут быть элементы, созданные разными способами.

Метод Row.AutoItem создает элемент без явно заданной ширины. Для таких элементов библиотека вычисляет внутренний размер их содержимого. Вычисленная ширина содержимого и есть ширина элемента. Обратите внимание, что элементы, созданные с помощью AutoItem, не переносят длинные строки.

Используйте метод ConstantItem, чтобы создать элемент с шириной, равной точному числу пунктов.

RelativeItem удобен, когда вы не знаете точные ширины элементов в строке и не хотите использовать внутренние размеры. Вместо этого можно задать относительную ширину элементов в строке. Метод принимает число частей, которые должен занимать элемент. Общее число частей — это сумма всех чисел во всех вызовах RelativeItem в этой строке.

Например, если вызов RelativeItem один, число неважно. Элемент займет всю доступную ширину. Для двух и более элементов числа задают пропорцию.

row.RelativeItem(2)
row.RelativeItem(3)
row.RelativeItem(1)

Все элементы, созданные приведенным выше кодом, занимают 6 частей (2 + 3 + 1 = 6). Отдельные элементы занимают соответственно 2 из 6, 3 из 6 и 1 из 6 частей.

Layout API использует следующую формулу для вычисления ширины одной части:

PartWidth = (RowWidth - AutoWidth - ConstantWidth) / TotalParts

где:
RowWidth = ширина контейнера строки
AutoWidth = ширина всех элементов, созданных с помощью метода AutoItem
ConstantWidth = ширина всех элементов, созданных с помощью метода ConstantItem

Вот пример, который создает строку с элементами всех трех типов.

var monthNames = DateTimeFormatInfo.InvariantInfo.MonthNames;
var groups = new[]
{
    string.Join(", ", monthNames.Take(4)),
    string.Join(", ", monthNames.Skip(4).Take(4)),
    string.Join(", ", monthNames.Skip(8).Take(4)),
};

PdfDocumentBuilder.Create().Generate("compounds-row.pdf", doc => doc.Pages(page =>
{
    page.Content()
        .Padding(20)
        .MinimalBox()
        .Row(row =>
        {
            row.ConstantItem(100)
                .Background(new PdfRgbColor(187, 237, 237))
                .Text("100 points wide");

            for (int i = 0; i < groups.Length; i++)
            {
                row.AutoItem().LineVertical(0.1);

                var numberOfParts = groups.Length - i + 1;
                row.RelativeItem(numberOfParts)
                    .PaddingHorizontal(5)
                    .Text(t =>
                    {
                        t.Line($"{numberOfParts} parts wide");
                        t.Line();
                        t.Line(groups[i]);
                    });
            }
        });
}));

Результат кода можно увидеть в compounds-row.pdf.

Column

Чтобы располагать элементы вертикально, один за другим, используйте контейнер Column. Каждый элемент в столбце является контейнером. Поэтому в одном столбце можно размещать содержимое разных типов.

Ширина каждого элемента равна ширине столбца. Высота каждого элемента зависит от его содержимого и свойств. Контейнеры Column поддерживают постраничную разбивку, поэтому надстройка Layout может выводить элементы столбца более чем на одной странице.

По умолчанию контейнер Column не содержит заголовка и нижнего колонтитула. Используйте методы Header и Footer, чтобы получить доступ к соответствующим контейнерам и настроить их. Если элементы столбца занимают более одной страницы, библиотека повторяет заголовки и нижние колонтитулы на каждой странице.

Используйте метод Spacing, чтобы добавить вертикальный интервал между элементами столбца. Обратите внимание, что библиотека не применяет интервал между заголовком и первым элементом. Библиотека также не добавляет отступ перед нижним колонтитулом.

PdfDocumentBuilder.Create().Generate("compounds-column.pdf", doc => doc.Pages(page =>
{
    page.Size(PdfPaperSize.A6);

    page.Content()
        .Padding(20)
        .Column(column =>
        {
            column.Header()
                .Background(new PdfRgbColor(187, 237, 237))
                .Padding(5)
                .Text("Month names");

            for (int i = 0; i < 12; i++)
            {
                column.Item().Background(
                    new PdfGrayColor(i % 2 == 0 ? 90 : 100))
                    .Padding(5)
                    .Text(DateTimeFormatInfo.InvariantInfo.MonthNames[i]);
            }

            column.Footer().LineHorizontal(1);
        });
}));

Результат кода можно увидеть в compounds-column.pdf.

Сетка

Макеты сетки организуют элементы в столбцы и строки. В этом смысле сетки похожи на таблицы. Layout API не предоставляет специального типа контейнера для сеток. Макет сетки можно реализовать с помощью контейнеров Column и Row.

Полезно представлять сетку как столбец, где каждый ее элемент — это строка. Оба контейнера Column и Row позволяют задавать интервал между элементами. При желании можно добавить заголовок и нижний колонтитул.

Каждая строка может иметь независимый макет. В каждой строке может быть разное количество элементов. Элементы могут иметь разную ширину и высоту. Можно добавить дополнительное пространство до, после или между элементами в строке. Для этого используйте элементы без содержимого и оформления.

var blue = new PdfRgbColor(187, 237, 237);
var darkerBlue = blue.Darken(50);
PdfDocumentBuilder.Create().Generate("compounds-grid.pdf", doc => doc.Pages(page =>
{
    page.Size(300, 200);

    page.Content().Padding(15).Column(column =>
    {
        column.Spacing(10);

        column.Item().Row(row =>
        {
            row.Spacing(10);

            row.ConstantItem(100).Background(darkerBlue).Height(40);
            row.RelativeItem(4).Background(blue);
        });

        column.Item().Row(row =>
        {
            row.Spacing(10);

            row.RelativeItem(2).Background(blue).Height(60);
            row.RelativeItem(1);
            row.RelativeItem(2).Background(blue);
        });

        column.Item().Row(row =>
        {
            row.Spacing(10);

            row.RelativeItem(1).Background(blue).Height(50);
            row.RelativeItem(3).Background(blue);
            row.ConstantItem(50).Background(darkerBlue);
        });
    });
}));

Результат кода можно увидеть в compounds-grid.pdf.

Списки

Списки повышают читаемость, разбивая информацию на краткие пункты. У элементов списка рядом с текстом могут быть номера, маркеры и другие символы. Макет списка можно легко реализовать с помощью контейнеров Column и Row. Layout API не предоставляет специального типа контейнера для списков.

Посмотрите пример кода, который создает список месяцев по сезонам. Обратите внимание: у списка есть заголовок. Если элементы содержат текст, который может переноситься на следующую строку, используйте для текстовой части элемента либо метод RelativeItem, либо метод ConstantItem.

var monthNames = DateTimeFormatInfo.InvariantInfo.MonthNames.ToList();
monthNames.Insert(0, monthNames[11]);

PdfDocumentBuilder.Create().Generate("compounds-list.pdf", doc => doc.Pages(page =>
{
    page.Size(150, 200);

    page.Content().Padding(5).Column(column =>
    {
        column.Header()
            .Text("Months by seasons:")
            .Style(TextStyle.Parent.Underline());

        for (int i = 0; i < 4; i++)
        {
            var season = string.Join(", ", monthNames.Skip(i * 3).Take(3));
            column.Item().Row(row =>
            {
                row.Spacing(2);

                row.AutoItem().Text("•");
                row.RelativeItem().Text(season);
            });
        }
    });
}));

Результат кода можно увидеть в compounds-list.pdf.

Table

Макеты таблиц организуют элементы в столбцы и строки. Тип контейнера Table предоставляет широкий набор возможностей и помогает в наиболее сложных случаях. Подробнее обо всех возможностях читайте в статье Контейнер таблицы.

InlineContainer

Можно заполнить контейнер коллекцией других контейнеров. Начните с вызова метода LayoutContainer.Inlined. Затем вызовите метод Item предоставленного InlineContainer, чтобы добавить дочерние контейнеры.

Надстройка Layout размещает контейнеры в строке один за другим. Если для элемента не хватает места, библиотека начинает новую строку. Используйте методы Spacing/HorizontalSpacing/VerticalSpacing, чтобы добавить промежуток между элементами.

Положение элементов в контейнере можно изменить с помощью методов выравнивания. Методы AlignTop/AlignMiddle/AlignBottom выравнивают элементы по вертикали. Для горизонтального направления используйте методы AlignLeft/AlignCenter/AlignRight/AlignJustify.

Есть особый случай. Метод AlignSpaceAround выравнивает элементы по горизонтали. Он также добавляет дополнительный интервал перед первым элементом и после последнего.

var orange = new PdfRgbColor(250, 123, 5);
var brown = orange.Darken(50);
var itemProps = new (int Width, PdfColor Color)[] {
    (20, orange), (30, orange), (50, brown), (50, orange), (50, orange),
    (30, orange), (20, brown), (30, orange), (50, brown), (10, brown)
};

PdfDocumentBuilder.Create().Generate("compounds-inlined.pdf", doc => doc.Pages(page =>
{
    page.Size(150, 120);

    page.Content().Inlined(c =>
    {
        c.Spacing(5);

        foreach (var (Width, Color) in itemProps)
            c.Item().Height(30).Width(Width).Background(Color);
    });
}));

Результат кода можно увидеть в compounds-inlined.pdf.

LayerContainer

Иногда требуется разместить часть содержимого ниже и/или выше основного содержимого страницы. Очевидный сценарий — добавить водяной знак поверх страниц PDF.

Сначала получите объект LayerContainer, вызвав метод LayoutContainer.Layers. Имея этот объект, можно начать добавлять слои. Вызов метода Layer добавляет дополнительный слой. Вызовите метод PrimaryLayer, чтобы добавить основной слой содержимого. Нужно добавить ровно один основной слой.

Layout API компонуeт слои в том же порядке, в котором вы их создаете. Слои, добавленные до основного слоя, попадут на задний план. Все слои, добавленные после основного слоя, окажутся над основным содержимым. Контейнер повторяет дополнительные слои на всех страницах, занятых основным содержимым.

Вот пример кода, показывающий, как добавлять водяные знаки на страницы PDF.

PdfDocumentBuilder.Create().Generate("compounds-layers.pdf", doc => doc.Pages(page =>
{
    var largeRedText = TextStyle.Parent.FontSize(48)
        .FontColor(new PdfRgbColor(235, 64, 52));

    page.Size(400, 250);

    page.Content()
        .Padding(25)
        .Layers(layers =>
        {
            layers.Layer()
                .AlignCenter()
                .Text(text => text.CurrentPageNumber().Style(largeRedText));

            layers.PrimaryLayer()
                .Background(new PdfGrayColor(85), 65)
                .Padding(25)
                .Text(new string('_', 790));

            layers.Layer()
                .AlignCenter()
                .AlignMiddle()
                .Text("Watermark")
                .Style(largeRedText);
        });
}));

Результат кода можно увидеть в compounds-layers.pdf.

Водяные знаки

Одно из распространенных требований — добавить водяной знак в PDF. Для этого могут быть разные причины. Можно добавить водяной знак в PDF, чтобы указать право собственности или обозначить конфиденциальность либо чувствительность информации в PDF.

Один из способов нанесения водяных знаков на PDF — использовать слои. См. пример в предыдущем разделе. Покажу другой подход.

Я буду использовать контейнеры фона и переднего плана страницы для нанесения водяных знаков на PDF. Надстройка Layout повторяет эти контейнеры на следующих страницах. Каждая страница с основным содержимым документа также будет содержать контейнеры фона и переднего плана. Это делает их подходящими для этой задачи.

Сначала я добавляю изображение в background container. Таким же способом можно добавить свой логотип в PDF. Изображение будет отображаться позади содержимого страницы. Неважно, сколько страниц документа используют фоновое изображение. API добавит в сгенерированный PDF только одну копию байтов изображения.

Основное содержимое документа может быть любым. Для этого примера кода я использую известный текст Lorem Ipsum.

Текстовый водяной знак помещается в foreground container. Сам текст может быть любым, и можно использовать любой цвет или шрифт. Я использовал повернутый текст, нарисованный полупрозрачными красными буквами увеличенного размера.

Пример кода асинхронно загружает и изображение, и текст из нашего репозитория с примерами кода. Конечно, можно использовать локальное изображение и/или читать текст из файла.

var urlPrefix =
    "https://raw.githubusercontent.com/BitMiracle/Docotic.Pdf.Samples/master/Samples/Sample%20Data/";

using var client = new HttpClient();
using var imageResponse = await client.GetAsync(urlPrefix + "watermark-background.png");
using var imageStream = await imageResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);

var loremIpsum = string.Empty;
using (var textResponse = await client.GetAsync(urlPrefix + "lorem-ipsum.txt"))
     loremIpsum = await textResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

PdfDocumentBuilder.Create().Generate("compounds-watermarks.pdf", doc =>
{
    var image = doc.Image(imageStream);

    doc.Pages(page =>
    {
        page.Size(400, 250);

        page.Background()
            .AlignMiddle()
            .Image(image);

        page.Content()
            .Padding(25)
            .Text(loremIpsum);

        var largeRedText = TextStyle.Parent.FontSize(48)
            .FontColor(new PdfRgbColor(235, 64, 52), 50);
        page.Foreground()
            .Rotate(-30)
            .Translate(-50, 180)
            .Text("DO NOT COPY")
            .Style(largeRedText);
    });
});

Результат кода можно увидеть в compounds-watermarks.pdf.

Примеры кода

У нас есть несколько примеров приложений, которые подробнее охватывают упомянутые возможности. Пожалуйста, уделите время их изучению.