TallPDF.NET

TallPDF.NET помогает формировать PDF-документы из данных приложения: собирать многостраничные отчёты из текста, таблиц и изображений, управлять колонтитулами и нумерацией, добавлять закладки, ссылки, поля форм и защиту, а затем записывать результат в файл, поток памяти или HTTP-ответ. Основные инструменты работы — объектная модель Document–Section–Paragraph, XML-описание макета, импорт XHTML с CSS и точное рисование через Drawing и Shape.

Типовой процесс начинается с создания объекта Document, добавления одной или нескольких секций и заполнения коллекции Paragraphs. Разметка не требует заранее вычислять координаты каждой строки: текст, таблицы и изображения последовательно перетекают между страницами, а размеры листа, поля, колонки, верхние и нижние области задаются на уровне секции. После построения модели метод Write выполняет компоновку и выводит готовый PDF в выбранный поток.

Рабочим интерфейсом служат классы и свойства, поэтому результат удобно собирать непосредственно из бизнес-данных: строки заказа превращать в Row и Cell, заголовки — в Heading, иллюстрации — в Image, а повторяемые реквизиты — в Header, Footer или Area. Для шаблонных документов доступен другой путь: структуру можно описать XML, подготовить её преобразованием XSL и при необходимости объединить декларативную часть с объектами, созданными в коде.

Скачать TallPDF.NET

Оценка 9.7 Рекомендуем
  • Редактирование PDF
  • Русский интерфейс
  • Просто новичкам
Скачать бесплатно на Windows
Лучшая альтернатива
TallPDF.NET
Оценка 8.5
  • Нет визуального редактора
  • XHTML не равен браузеру
  • Часть API по лицензии
Скачать TallPDF.NET
Загрузка начнётся после нажатия

Как построить первый документ

Минимальная модель состоит из Document, Section и хотя бы одного абзаца. Section создаёт страницы только тогда, когда содержимое начинает раскладываться, поэтому не требуется вручную добавлять Page для каждой порции данных. В TextParagraph помещают коллекцию Fragment: каждый фрагмент хранит собственный текст, шрифт, размер, цвет и начертание. Затем абзац добавляют в Section.Paragraphs, а Document.Write получает FileStream, MemoryStream или поток ответа веб-приложения.

var document = new Document();
var section = document.Sections.Add();
var paragraph = new TextParagraph();
paragraph.Fragments.Add(new Fragment("Счёт на оплату", Font.HelveticaBold, 16));
section.Paragraphs.Add(paragraph);
using var output = File.Create("invoice.pdf");
document.Write(output);

Важная практическая деталь: объекты изображений и потоки, из которых они читаются, должны оставаться доступными до завершения Write, если выбран перегруженный конструктор без передачи владения ресурса. Закрытый MemoryStream с картинкой или удалённый временный файл приведёт к ошибке уже на этапе рендеринга, хотя модель документа была собрана без исключений. Поэтому временные ресурсы освобождают после записи PDF либо используют перегрузки, явно передающие ответственность компоненту.

Пример объектной модели TallPDF.NET с Document, Section, TextParagraph, Image и Table

Документ, метаданные и режим открытия

Свойство DocumentInfo предназначено для сведений, которые PDF-просмотрщик показывает в окне свойств: названия, автора и других полей описания. Эти значения лучше заполнять из тех же данных, что используются в видимом заголовке отчёта, иначе файл с корректным содержанием будет иметь неинформативное имя документа в поиске и системе хранения. Метаданные задаются до вызова Write; их изменение после формирования потока уже не влияет на записанный файл.

ViewerPreferences управляет начальным представлением: раскладкой страниц, масштабом, полноэкранным показом и видимостью панелей в совместимом просмотрщике. Такие настройки являются рекомендацией для программы чтения PDF, а не гарантией одинакового поведения во всех клиентах. Для каталога удобно задать одиночную страницу, для буклета — двухколоночную раскладку, но основной макет документа нельзя строить в расчёте на то, что любой просмотрщик обязательно применит эти предпочтения.

Document также связывает действия с событиями открытия, печати, сохранения и закрытия. Сам генератор их не исполняет: он записывает команды в PDF, а решение о запуске принимает просмотрщик. По этой причине сценарий, который заполняет поле перед печатью, нужно тестировать в целевом клиенте. В браузерных просмотрщиках JavaScript PDF часто ограничен, поэтому критичные вычисления следует выполнять в приложении до формирования файла, а не переносить в действие документа.

Секции, формат страницы и поля

Секция определяет общие параметры порождённых ею страниц. PageSize принимает стандартные форматы вроде A4 и Letter либо пользовательскую ширину и высоту. Margin.Left, Right, Top и Bottom задают рабочую область потока. Верхнее и нижнее поля одновременно резервируют пространство для колонтитулов, поэтому слишком маленький отступ может привести к наложению основной части на Header или Footer. Для документов с разной ориентацией создают несколько секций: например, титульный лист и текст оставляют книжными, а широкую сводную таблицу помещают в отдельную секцию с альбомными размерами.

Размеры выражаются в пунктах или через Unit с указанием дюймов и других единиц. В проекте лучше выбрать один способ и применять его последовательно. Смешивание чисел, подразумевающих пункты, и Unit без явного комментария затрудняет поиск причины смещений. Для печатных форм полезно вынести поля и размеры в единый класс настроек, чтобы изменение корпоративного шаблона не требовало искать значения по десяткам обработчиков.

CropBox ограничивает область, которую предполагается показывать и печатать, а BleedBox описывает вылеты для послепечатной обработки. Эти параметры полезны при выпуске материалов с метками реза и фоном до края листа. Если задача ограничена офисной печатью, достаточно PageSize и полей; случайно заданный CropBox способен скрыть элементы, физически присутствующие в MediaBox, поэтому его не следует использовать как замену обычным отступам.

Колонки и смена макета

Section.Columns позволяет распределить поток по нескольким вертикальным колонкам. Для каждой колонки задаётся ширина, а у предыдущей — интервал до следующей. Абзацы заполняют первую колонку сверху вниз, затем переходят в следующую и только после этого создают новую страницу. Такой режим подходит для бюллетеней, справочников и компактных перечней, но широкие таблицы в многоколоночной секции часто выходят за доступную область. Практичнее вынести таблицу в соседнюю секцию с одной колонкой.

Новая секция обычно используется и тогда, когда меняются колонтитулы, поля или формат бумаги. В обычном режиме свойство StartOnNewPage помогает управлять переходом, однако при потоковой событийной генерации оно игнорируется: каждая секция начинает новую страницу. Если нужен динамический формат без жёсткого разбиения, параметры страницы следует возвращать в обработчике QueryPageSettings.

Колонтитулы и нумерация страниц

Для чётных и нечётных страниц предусмотрены отдельные EvenHeader, OddHeader, EvenFooter и OddFooter. Содержимое каждой области — обычная коллекция Paragraph, поэтому туда можно добавить текст, изображение, горизонтальную линию, таблицу или Drawing. Колонтитул течёт внутри своей области так же, как основное содержимое; если он слишком высок, часть элементов может выйти за резерв, заданный полем секции. Высоту и верхний отступ Header стоит проверять на страницах с максимальным набором реквизитов.

Номер текущей страницы и общее число страниц вставляются контекстными полями. Фрагмент с шаблоном наподобие #p из #P помечают как содержащий контекстные значения, после чего при рендеринге подстановки превращаются в числа. Поле общего количества требует знания финальной пагинации, поэтому оно несовместимо с частью сценариев однонаправленной генерации, где страницы сразу сбрасываются в поток и не хранятся для второго прохода.

Для титульной страницы без номера удобно создать отдельную секцию либо применить области с признаками первой страницы. Если нумерация должна продолжаться между секциями, проверяют стартовое значение счётчика и поведение контекстных полей на границе. Ошибка, при которой 1 появляется снова после альбомной вставки, обычно связана не с Footer, а с настройками новой секции.

Абзацы и управление разрывами

Paragraph — общий предок для текста, таблиц, рисунков, изображений, линий, заголовков и других блоков. От него наследуются HorizontalAlignment, SpacingBefore, SpacingAfter и KeepWithNext. Благодаря этому расстояние между блоками задаётся на уровне компоновки, а не пробельными строками. Пустые TextParagraph ради отступа ухудшают предсказуемость: они участвуют в переносах и могут остаться в начале следующей страницы.

KeepWithNext связывает текущий блок со следующим и полезен для заголовка перед таблицей или подписи перед иллюстрацией. Эту настройку нельзя бездумно включать для длинной цепочки: если несколько крупных элементов требуют оставаться вместе и суммарно не помещаются на страницу, компоновщик оказывается перед невыполнимым условием. Связку применяют только там, где объединённая высота заведомо меньше рабочей области.

Горизонтальное выравнивание влияет на блок целиком, а не на символы внутри строки. Для текста дополнительно используется собственное выравнивание и Justification, для изображения — ширина и политика сохранения пропорций, для таблицы — алгоритм расчёта колонок. При поиске причины смещения сначала определяют, на каком уровне задано свойство: Paragraph, TextParagraph, Fragment, Cell или Shape.

Текст и смешанное форматирование

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

Justification увеличивает интервалы между словами, чтобы строка заняла всю ширину. Для узких колонок это может давать заметные пробелы, особенно при длинных терминах без возможности переноса. В таблицах и подписях чаще лучше левое выравнивание. Подчёркивание, зачёркивание, цвет, жирное и курсивное начертание задаются на уровне фрагмента; размер шрифта следует выбирать с учётом фактического шрифта, потому что одинаковый кегль разных гарнитур даёт различную высоту и ширину.

Для многоязычных отчётов критично загрузить шрифт, содержащий нужные глифы. Стандартные четырнадцать PDF-шрифтов удобны для латиницы, но не покрывают весь набор кириллицы и специальных символов. Если в результате появляются квадраты или пропуски, проблема обычно не в кодировке строки .NET, а в выбранной гарнитуре или способе её встраивания. Файл шрифта должен быть доступен процессу, а лицензия шрифта — разрешать встраивание.

Измерение текста до записи

При точной компоновке, например в штрихкодной этикетке или бланке с фиксированными рамками, полезно измерять итоговые размеры после назначения Font, FontSize и Text. TextShape предоставляет Width и Height для однострочного текста, а MultilineTextShape — MeasuredHeight после учёта ширины и переносов. Измерение до установки шрифта возвращает данные для других параметров и приводит к неверному центрированию.

Для обычного потока лучше не вычислять координаты каждой строки. TextParagraph переносит текст автоматически и устойчивее к изменению данных. Shape оправдан там, где элементы должны совпадать с заранее напечатанной формой, размещаться на этикетке или составлять графическую схему. Смешивание потоковой и координатной модели возможно: Drawing добавляется как абзац и занимает рассчитанное место между другими блоками.

Таблицы, строки и ячейки

Table наследует Paragraph, состоит из Row, а каждая строка — из Cell. В ячейку помещается коллекция абзацев, поэтому там доступны текст, картинки, вложенные таблицы и Drawing. Это позволяет строить не только сетку данных, но и сложную карточку с реквизитами, миниатюрой и подтаблицей. Вложенность следует ограничивать разумно: несколько уровней усложняют расчёт минимальной ширины и делают поведение при переносах менее очевидным.

Объектная модель таблицы TallPDF.NET: Table, Row, Cell и Paragraph

Границы представлены отдельными перьями для сторон, фон — кистью. Margin находится снаружи границы, Padding — между границей и содержимым. Эти отступы могут существовать у таблицы, строки и ячейки одновременно, поэтому итоговое свободное место складывается. Когда текст неожиданно получает большой зазор, проверяют все три уровня, а не только Cell.Padding.

Соотношение margin, padding и border у таблицы TallPDF.NET

Как рассчитывается ширина колонок

Явного объекта Column в модели нет: колонка возникает из позиции ячеек в строках. Сначала вычисляется минимальная ширина каждой ячейки. Для текста нижней границей служит самое широкое слово, для Image и Drawing — их Width, для вложенной Table — собственный результат расчёта. Затем учитываются Fixed, FitToContent и PreferredWidth. Максимальные значения ячеек на одинаковой позиции определяют минимальную и предпочтительную ширину всей колонки.

Если Table.ForceWidth включён, итоговая ширина таблицы переопределяет предпочтения ячеек, а свободное пространство распределяется между колонками. Если суммарная предпочтительная ширина больше рабочей области, колонки уменьшаются, но не ниже минимальной. В крайнем случае, когда даже минимальные размеры превышают страницу, таблица пересекает правое поле. Исправление состоит не в уменьшении PageSize после факта, а в сокращении неразрывных строк, выборе меньшего шрифта, изменении PreferredWidth, переносе таблицы в альбомную секцию или разделении набора колонок.

RepeatFirstRow повторяет первую строку на каждой странице длинной таблицы. Row.DoNotBreak пытается сохранить строку целиком. Если строка выше доступной области страницы, запрет разрыва невозможно выполнить; поэтому в ячейки с большими текстами или несколькими изображениями не стоит помещать чрезмерный объём под этим флагом.

Изображения и многостраничный TIFF

Image принимает путь к файлу, адрес ресурса, Stream или System.Drawing.Bitmap. Поддерживаются растровые форматы BMP, PNG, TIFF, JPEG и GIF. Для данных из базы обычно создают MemoryStream из BLOB и передают его конструктору. Для веб-приложения безопаснее загружать ресурс самим, контролировать тайм-аут и размер, а затем формировать Image из потока, чем разрешать шаблону обращаться к произвольным сетевым адресам.

Width и Height задают размеры в макете. При включённом KeepAspectRatio изменение одной стороны пересчитывает вторую. Если заданы обе стороны, картинка вписывается или растягивается согласно текущим параметрам. FitPolicy.Shrink полезен, когда изображение может оказаться шире ячейки: вместо исключения оно уменьшается до доступной области. Однако автоматическое уменьшение не исправляет низкое исходное разрешение; крупная фотография, сжатая до миниатюры, также увеличивает размер PDF, если предварительно не оптимизировать её данные.

Для TIFF доступен выбор кадра через FrameIndex и число кадров через FrameCount. Конвертацию многостраничного TIFF выполняют циклом: для каждого кадра создают Image, при необходимости добавляют разрыв страницы и помещают изображение в секцию. Нумерация кадров начинается с нуля, поэтому условие цикла должно быть строго меньше FrameCount.

Drawing и координатные фигуры

Drawing создаёт холст заданной ширины и высоты, а Shapes хранит элементы с координатами. Базовая Shape управляет положением и Dock, ContentShape добавляет вращение. Доступны ImageShape, TextShape, MultilineTextShape, SimpleXhtmlShape, PageShape, группы ShapeCollection, линии, кривые Безье, прямоугольники, эллипсы, дуги и сектора. Такой набор подходит для бланков, диаграмм, этикеток, штампов и фоновых элементов, где потокового абзаца недостаточно.

Иерархия фигур Shape в TallPDF.NET

ShapeCollection сама является фигурой и вводит собственную систему координат. Width и Height задают размер в родительской системе, VirtualWidth и VirtualHeight — пространство, которое видят дочерние элементы. Разница между ними создаёт масштабирование группы. Clip ограничивает вывод прямоугольником коллекции. Это удобно для повторно используемого компонента: диаграмму можно проектировать в условных координатах 1000×1000 и размещать в разных размерах без пересчёта каждой точки.

Вложенные ShapeCollection и виртуальные координаты TallPDF.NET

Контуры, перья и заливки

PathShape объединяет LineShape и BezierShape в произвольный открытый или закрытый контур. RectangleShape, EllipseShape, ArcShape и PieShape дают готовые геометрические примитивы. Pen задаёт толщину, цвет, окончания, соединения и штриховой рисунок. Массив dashes чередует длины видимого и пустого участков, а phase определяет начальную позицию в этом шаблоне. Ошибка в массиве обычно проявляется не исключением, а неожиданным ритмом линии, поэтому параметры удобно проверять на коротком тестовом контуре.

Иерархия PathShape и геометрических фигур TallPDF.NET

Параметры phase и dashes для пера TallPDF.NET

SolidBrush заполняет область одним цветом, градиентные кисти создают переход по прямой или между окружностями. Если контур не замкнут, заливка может дать результат, отличный от ожидаемого. Для прозрачного водяного знака обычно достаточно TextShape с Opacity и вращением; сложная кисть нужна для графики, а не для полупрозрачного текста.

Штрихкоды и поля формы

BarcodeShape служит общей основой для одномерных штрихкодов. В документации представлены Code 128, Interleaved 2 of 5 и Code 3 of 9. Штрихкод размещается как фигура, поэтому его можно совместить с текстовой подписью, рамкой и другими элементами на Drawing. Перед массовой печатью следует проверить допустимый набор символов выбранного стандарта, минимальную ширину модуля и наличие свободной зоны; слишком сильное масштабирование делает код визуально корректным, но плохо читаемым сканером.

Иерархия BarcodeShape в TallPDF.NET

FieldShape создаёт интерактивные поля: однострочное текстовое поле, флажок, радиокнопку, список, раскрывающийся список, кнопку и пустое поле цифровой подписи. Одинаковое FullName объединяет радиокнопки в группу, где выбирается один вариант. PushButton может сбрасывать форму, отправлять данные или запускать другое действие. Поддерживаемый формат отправки и поведение кнопки зависят от PDF-просмотрщика, поэтому веб-анкеты надёжнее проверять не только в одном настольном клиенте.

Пустое SignatureFieldShape резервирует область для последующей подписи, но не заменяет процедуру создания криптографической подписи с сертификатом. Если бизнес-процесс требует юридически значимой подписи, после генерации понадобится этап подписания подходящим средством и проверка, что последующие изменения документа не нарушают подпись.

Области, подложки и водяные знаки

Area — прямоугольная зона с собственной коллекцией Paragraph. Header и Footer являются специализированными областями. У секции есть фоновые и передние области, которые повторяются на подходящих страницах. BackgroundArea удобно применять для фирменного бланка, рамки или фоновой страницы; ForegroundArea — для штампа Конфиденциально, номера экземпляра или служебной отметки поверх содержимого.

Координаты Area нужно сопоставлять с системой страницы и высотой области. В примере с диагональным штампом Drawing занимает всю страницу, TextShape получает крупный шрифт, полупрозрачность и отрицательный угол. Если знак обрезается, проверяют не только X и Y, но и размеры Drawing, Clip у вложенной ShapeCollection и расположение в foreground/background коллекции.

PageShape помещает страницу существующего PDF как графический примитив без потери исходных данных при масштабировании. Для включения в оригинальном размере создают секцию с тем же PageSize, обнуляют поля, растягивают Drawing на страницу и добавляют PageShape. Этот механизм подходит для использования готового бланка как основы, но операции чтения, перестановки и полноценного редактирования существующего PDF относятся к другому классу API.

Закладки, подписи и перекрёстные ссылки

Heading, BookmarkParagraph и свойства закладок формируют дерево навигации. Для отчёта полезно назначать закладку крупным разделам, а не каждой строке: перегруженная панель навигации ухудшает поиск. Текст закладки может включать контекстные поля, чтобы номер и подпись совпадали с видимым заголовком.

Label и Caption описывают тип и название объекта. Изображению можно присвоить Label Рисунок и Caption с содержательным описанием, а соседний Fragment с шаблоном #l #0. #c автоматически получит метку, порядковый номер и подпись. Ссылка на объект через Reference позволяет дополнить фразу номером страницы. Так исключается дублирование строк, которое часто приводит к расхождению подписи под рисунком и записи в списке иллюстраций.

CrossreferenceSection предназначен для автоматически сформированных оглавлений, списков таблиц и рисунков. Поскольку номера страниц известны только после раскладки, такие разделы требуют режима, допускающего разрешение контекстных полей. В событийной однонаправленной генерации перекрёстные ссылки и контекстные поля недоступны; для длинного документа приходится выбирать между минимальной памятью и автоматическим оглавлением либо строить индекс отдельным проходом.

Переходы и действия

GoToAction переносит пользователя к абзацу внутри документа, странице текущего файла или странице внешнего PDF. ParagraphDestination устойчивее номера страницы: при изменении объёма предыдущих разделов цель остаётся привязана к смысловому объекту. InternalPageDestination использует индекс страницы и потому менее удобен в потоковой модели, где точная пагинация заранее неизвестна.

UriAction открывает адрес, JavaScriptAction выполняет сценарий в поддерживающем клиенте. Действия назначаются документу, LinkShape, кнопке формы, Fragment или Paragraph. Для текстовой ссылки удобно связывать действие с конкретным Fragment, чтобы кликабельной была только подпись, а не весь абзац. В публичных отчётах следует избегать адресов, полученных напрямую из пользовательских данных: их нужно валидировать и разрешать только ожидаемые схемы.

ResetFormAction возвращает значения полей к исходным, SubmitFormAction отправляет данные формы в выбранном формате. Сетевой запрос выполняет просмотрщик, не сервер, который сформировал PDF. Корпоративные политики могут блокировать отправку, а пользователь увидит предупреждение. Поэтому форму с обязательной доставкой данных не стоит строить исключительно на SubmitFormAction без альтернативного канала.

Генерация по XML и преобразование XSL

Вместо ручного создания каждого объекта модель Document можно загрузить целиком или частично из XML. После загрузки это те же классы, что были бы созданы C#: к ним можно обращаться, добавлять секции и менять свойства. Такой подход разделяет данные, шаблон и программную логику. Например, бухгалтерская система формирует XML заказа, XSL преобразует его в схему TallPDF.NET, а код только подставляет изображения, применяет защиту и записывает результат.

XML должен описывать точные типы и свойства, поэтому опечатка в имени класса или несоответствие пространства имён приводит к ошибке загрузки, а не к частичному игнорированию разметки. Для устойчивого процесса полезно валидировать промежуточный XML, хранить тестовые примеры с пустыми и максимальными значениями и фиксировать используемую схему. XSL следует проверять отдельно от рендеринга: если строка отсутствует уже в преобразованном XML, искать её в компоновщике бессмысленно.

Наследованные пользовательские классы также могут участвовать в XML. Специализированный Heading способен заранее задать Label, формат закладки и набор Fragment, а шаблон указывает его полное имя. Это сокращает декларативную разметку и обеспечивает единый стиль, но связывает XML с конкретной сборкой и пространством имён. При рефакторинге такие имена нужно сохранять или мигрировать шаблоны.

Импорт XHTML и CSS

XhtmlParagraph принимает строку, поток или путь к XHTML и преобразует поддерживаемую разметку в PDF. Заявлена работа с XHTML 1.0 Strict, XHTML 1.1 и CSS 2.1, включая разрывы страниц, формы и ссылки. Класс остаётся абзацем, поэтому его можно поместить в секцию, ячейку, Header или Footer и окружить обычными объектами TallPDF.NET.

Вход должен быть корректным XML: все элементы закрыты, теги вложены без пересечений, значения атрибутов заключены в кавычки. Обычная веб-страница может содержать терпимые браузером ошибки и динамически созданные узлы, но такой код не обязан загружаться как XHTML. Перед конвертацией полезно нормализовать шаблон, а данные экранировать, чтобы амперсанд или угловая скобка из пользовательского текста не разрушили документ.

Результат не гарантирует пиксельное совпадение с браузером. JavaScript, динамический контент, ауральные свойства, direction и unicode-bidi не поддерживаются; документ ограничен одной формой. Для контролируемых шаблонов с известным CSS это рабочий путь, а произвольный сайт лучше не использовать как исходную разметку без предварительного теста. ContentFitBehavior позволяет оставить масштаб 96 пикселей к 72 пунктам, задать фиксированную ширину, вписать по ширине или уменьшить до одной страницы. Неверный режим приводит либо к выходу за правый край, либо к слишком мелкому тексту.

Ресурсы XHTML

Изображения и другие ресурсы могут загружаться стандартным механизмом либо через пользовательский IResourceLoader. Такой загрузчик получает первый шанс вернуть поток для каждого ресурса и подходит для файлов, защищённых авторизацией, хранящихся в базе или доступных по внутреннему идентификатору. Если он возвращает null, обработка передаётся штатному механизму.

Пользовательский загрузчик должен ограничивать объём и время чтения. Иначе одна ссылка в шаблоне способна заставить сервер ждать недоступный узел или загрузить чрезмерный файл. Для повторяемых логотипов полезен кэш байтов, но передавать один и тот же Stream нескольким объектам без сброса Position нельзя: последующее чтение начнётся с конца.

Событийная генерация больших документов

Document.Write имеет режим, в котором содержимое запрашивается обработчиками по мере формирования страниц. Завершённая страница сразу выводится, поэтому память удерживает в основном ресурсы одной страницы. Этот вариант полезен для реестров с сотнями тысяч строк и серверов, одновременно создающих несколько отчётов. Он не включается автоматически: выбирается перегрузкой Write с соответствующим флагом.

Преимущество оплачивается ограничениями однонаправленного потока. Cell.ColSpan и FitToContent игнорируются, ширины задают заранее. Каждая секция начинает новую страницу. Нельзя ссылаться из Fragment на абзацы, а контекстные поля не разрешаются. Следовательно, автоматическое оглавление, страница X из Y и часть сложных таблиц требуют обычного режима. Решение принимают по реальному документу, а не только по объёму данных.

В обработчиках QuerySection, QueryParagraphs и событий страницы данные следует выдавать порциями. Если заранее загрузить весь набор из базы в List, экономия памяти генератора не поможет. Лучше читать записи DataReader или постраничным запросом, создавать ограниченное число Row, отдавать их и освобождать связанные изображения. Обработчики должны быть детерминированными: повторный запрос к внешнему поставщику данных в середине генерации способен вернуть изменившиеся данные.

Потоки, веб-ответ и фоновые задания

Запись в MemoryStream удобна, когда PDF нужно вернуть из контроллера, приложить к письму или сохранить в объектном хранилище. После Write позиция потока находится в конце; перед повторным чтением её устанавливают в ноль либо получают ToArray. При больших файлах лишняя копия ToArray заметно увеличивает память, поэтому поток лучше передавать напрямую, когда принимающий API это допускает.

При выводе в HTTP-ответ заголовок Content-Type задают как PDF, имя файла кодируют безопасно, а обработку исключения завершают до отправки части тела. Если сервер успел записать первые байты и затем возникла ошибка изображения, вернуть клиенту нормальный JSON уже нельзя. В критичных сценариях документ формируют во временный поток, проверяют и только потом копируют в ответ.

Фоновое задание должно получать все входные данные явно. Объекты HTTP-контекста, открытые соединения и пользовательские потоки не следует сохранять до выполнения очередью. Путь к шрифту и изображениям строят от каталога приложения, а не от текущей рабочей директории, которая различается в службе, тесте и локальном запуске.

Практический сценарий: счёт или акт

Реквизиты продавца удобно разместить в двухколоночной Table: слева логотип и адрес, справа номер, дата и статус. Перечень позиций строится отдельной таблицей с повторяемой первой строкой. Для описания товара используют ячейку с гибкой шириной, для количества и суммы — Fixed или PreferredWidth. Денежные значения форматируют до добавления Fragment, чтобы разделитель и число знаков после запятой были единообразны.

Итоги помещают в компактную вложенную таблицу, выровненную вправо. Заголовок Итого связывают с таблицей через KeepWithNext, чтобы он не остался внизу страницы отдельно. Подписи и печать размещают в Drawing, если требуются точные координаты, либо обычной таблицей, если блок должен естественно переноситься. Номер документа и дата входят и в видимый заголовок, и в DocumentInfo.

Чтобы не получить пустую последнюю страницу, не добавляют безусловный разрыв после каждой записи. Разрыв нужен перед логическим разделом или следующим документом в пакетной выдаче. При формировании нескольких счетов в одном PDF удобно создавать отдельную секцию для каждого и явно начинать её с новой страницы.

Практический сценарий: длинный реестр

Для реестра сначала определяют набор колонок и максимальную длину значений. Если идентификатор может содержать длинную неразрывную строку, она задаёт минимальную ширину и способна вытолкнуть таблицу за поле. Такой текст заранее сокращают, разрешают перенос подходящим разделителем или выводят в отдельной строке. Заголовок таблицы делают первой строкой и включают RepeatFirstRow.

Row.DoNotBreak применяют к коротким строкам, но отключают для поля с многострочным комментарием. Чередование фона строк создают при построении Row, не отдельными Drawing: так цвет следует за фактической высотой строки. Для сотен тысяч записей используют событийный режим, однако заранее отказываются от ColSpan и FitToContent либо заменяют их фиксированными ширинами.

Страницу и диапазон записей можно выводить в Footer, но общее количество страниц в потоковом режиме недоступно. Если X из Y обязательно, документ строят обычным способом или выполняют дополнительный проход с промежуточным хранилищем. Выбор следует сделать до реализации шаблона, поскольку переход между режимами влияет на таблицы и ссылки.

Практический сценарий: каталог с изображениями

Карточку товара можно реализовать как Table с двумя Cell: изображение фиксированной ширины и текстовый набор TextParagraph справа. FitPolicy.Shrink защищает от слишком большого изображения, KeepAspectRatio сохраняет пропорции. Ограничивать нужно и высоту: очень высокая вертикальная фотография способна растянуть строку на страницу и оставить рядом большой пустой участок.

Перед созданием Image проверяют Content-Type и фактический формат, а также задают верхний предел пикселей. Ошибочное расширение файла не гарантирует, что декодер сможет прочитать данные. Повторяющиеся изображения кэшируют как байты или безопасно создаваемые объекты, но не сохраняют Bitmap без освобождения на весь каталог.

Подпись связывают с иллюстрацией через Caption и контекстный Fragment, если нужен список рисунков. Для простого каталога лучше не создавать сотни закладок на каждую фотографию; достаточно закладок категорий и ссылок из оглавления. Размер итогового PDF сильнее зависит от исходных растров, чем от числа текстовых объектов, поэтому оптимизацию начинают с изображений.

Совместимость проекта и подключение

Официальный пакет предоставляет сборки для .NET Framework 4.0 и .NET Standard 2.0. Для проекта выбирают вариант, соответствующий целевому framework, и проверяют транзитивные зависимости. Автоматически вычисленная NuGet совместимость с более новыми .NET не означает, что каждый сценарий проверен на каждой платформе, поэтому работу со шрифтами, System.Drawing и сетевыми ресурсами испытывают в фактической среде развёртывания.

Пакет добавляют через PackageReference, после чего импортируют пространства имён TallComponents.PDF.Layout и нужных групп Paragraphs, Fonts, Shapes. При наличии других PDF-библиотек важно не смешивать одноимённые типы из разных DLL: похожие названия классов не делают объекты взаимозаменяемыми. В проекте фиксируют один согласованный набор зависимостей и очищают лишние ссылки из bin, obj и конфигурации публикации.

Для Linux-контейнера проверяют доступность шрифтов и участки, использующие System.Drawing.Bitmap. Даже когда основное ядро совместимо через .NET Standard, графический API и нативные кодеки могут отличаться. Надёжный тест должен запускаться в том же образе контейнера и сравнивать не только факт создания файла, но и наличие нужных глифов и изображений.

Защита и права PDF

PasswordSecurity позволяет задать пользовательский и владельческий пароли и ограничения, например запрет печати. Распространённый сценарий — пустой пользовательский пароль и непустой владельческий: файл открывается без запроса, но изменение разрешений требует секрета. Эти ограничения реализуются средствами PDF и зависят от соблюдения правил просмотрщиком; они не заменяют контроль доступа к исходному файлу.

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

При генерации из XHTML и XML необходимо экранировать данные. UriAction и пути к изображениям валидируют, пользовательский IResourceLoader ограничивает доступ только разрешёнными каталогами и хостами. Генератор не должен становиться способом читать локальные файлы по пути, пришедшему из шаблона или запроса.

Диагностика типовых ошибок

Изображение не помещается

Исключение при добавлении картинки обычно означает, что её рассчитанная ширина или высота превышает доступную область секции, ячейки или Area. Проверяют Width, Height, KeepAspectRatio, отступы и границы контейнера. Для допустимого автоматического уменьшения назначают FitPolicy.Shrink; если качество стало неудовлетворительным, готовят отдельную оптимизированную копию изображения.

Таблица выходит за правый край

Причина — сумма минимальных ширин колонок больше страницы. Ищут длинные слова, фиксированные ячейки, вложенные таблицы и накопленные padding/margin. ForceWidth не способен сжать колонку ниже её минимума. Рабочие решения: альбомная секция, меньше колонок, перенос данных, сокращение шрифта или предварительное разбиение таблицы.

Кириллица отображается квадратами

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

XHTML отличается от страницы в браузере

Проверяют корректность XML, поддержку используемого CSS, отсутствие JavaScript и динамического DOM, режим ContentFitBehavior и загрузку ресурсов. Конвертируемый шаблон следует упростить до поддерживаемого подмножества, а не пытаться воспроизвести сложное веб-приложение.

Пустой или повреждённый PDF

Убеждаются, что Write завершился без исключения, выходной поток не был закрыт заранее, а при чтении MemoryStream позиция возвращена к началу. Если файл отдаётся по HTTP, проверяют отсутствие текстовых сообщений, BOM или HTML-ошибки перед сигнатурой PDF. В лог записывают исключение и идентификатор задания, но не содержимое конфиденциального документа.

Контекстное поле осталось текстом

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

Память растёт на большом отчёте

Проверяют не только режим Write, но и собственный код: загружен ли весь набор данных, удерживаются ли Bitmap, MemoryStream и большие byte[], кэшируются ли все строки таблицы. Событийный режим уменьшает память компоновщика, но не освобождает ресурсы, на которые приложение продолжает ссылаться.

Действие или форма не срабатывает

PDF-просмотрщик может блокировать JavaScript, сетевую отправку или часть событий. Проверяют файл в целевом клиенте, а обязательную бизнес-логику переносят на сервер. Поле с одинаковым FullName может объединиться с другим, поэтому имена формируют уникально и предсказуемо.

Расширение объектной модели

Классы можно наследовать и превращать повторяющийся набор настроек в собственный компонент. Специализированный Heading способен в конструкторе назначить уровень, Label, Bookmark и формат Fragment, а наружу вывести простое свойство Text, связанное с Caption. Такой объект используется как обычный Paragraph и поддерживает потоковый макет.

Пользовательский ShapeCollection подходит для диаграммы или фирменного штампа. Внутри он строится из примитивов, снаружи предоставляет предметные свойства: значение, подпись, диапазон, цвет. VirtualWidth делает компонент масштабируемым. Это безопаснее, чем копировать координаты фигур в каждом отчёте.

Наследование не должно скрывать тяжёлые операции. Если конструктор загружает сеть или базу, ошибка возникает при построении модели в неожиданном месте. Лучше передавать готовые данные и отделять их получение от представления. Пользовательские классы, используемые из XML, сохраняют стабильные имена и имеют понятные значения по умолчанию.

Иерархия заголовков, подписи и автоматическое оглавление

Heading отличается от обычного TextParagraph не внешним видом, а участием в структуре документа. Уровень заголовка задаёт его место в иерархии, Caption хранит содержательное название, Label — тип элемента, а Bookmark определяет текст, который попадёт в дерево закладок PDF-просмотрщика. Поэтому номер главы, подпись на странице и название закладки можно формировать из одних данных, не копируя строку в три независимых места. На практике для каждого уровня создают фабрику, которая назначает шрифт, интервалы, KeepWithNext и шаблон Bookmark, а вызывающий код передаёт только Caption.

Контекстные поля превращают служебные маркеры во время компоновки. Поле #p подставляет номер текущей страницы, #P — общее число страниц, #c — подпись связанного объекта, #l — его метку, а числовые поля извлекают номер заголовка или подписанного абзаца. Формат нумерации можно менять между арабскими числами, буквами и римскими обозначениями. Чтобы поле было вычислено, у Fragment включают обработку контекста либо назначают Reference или GoToAction на целевой Heading, Table, Image или другой Paragraph. Если ссылка не установлена, выражение, рассчитанное на связанный объект, не знает, откуда брать Caption и страницу.

CrossreferenceSection предназначен для оглавления, списка таблиц и списка рисунков. Компонент вызывает ComposeEntry для абзацев документа, а обработчик решает, какие элементы включить и как оформить строку. Для оглавления обычно пропускают всё, кроме Heading нужных уровней, создают TextParagraph, добавляют Fragment с шаблоном номера, подписи и страницы, а к нему присоединяют GoToAction на исходный заголовок. Для списка иллюстраций фильтруют Paragraph по Label и используют Caption. Такой способ надёжнее ручного массива страниц: фактическая страница определяется после переноса текста и изменения высоты предыдущих блоков.

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

Проектирование XHTML-шаблона под предсказуемый PDF

XhtmlParagraph принимает хорошо сформированный XHTML, а не произвольный HTML из браузера. Все элементы должны быть закрыты, вложенность — сбалансирована, значения атрибутов — заключены в кавычки. До передачи разметки полезно разобрать её XML-парсером: ошибка будет обнаружена возле исходного шаблона, а не в середине записи PDF. Разметку, полученную из редактора или CMS, очищают от неподдерживаемых элементов, исправляют одиночные теги и формируют корневой html с пространством имён.

CSS следует ограничить проверенным набором правил. Конвертер ориентируется на CSS 2.1 и собственную модель страниц, поэтому браузерные механизмы сценариев, динамический DOM и современные раскладки не являются частью процесса. Стили размещают в шаблоне или подключают через контролируемые ресурсы, а ConversionSettings переиспользуют для одинаковых XhtmlParagraph. Это помогает согласовать базовые шрифты, обработку таблиц и другие параметры во всех фрагментах документа.

Сложный отчёт не обязательно целиком переводить в XHTML. XhtmlParagraph можно добавить в Section, Cell, Header или Footer как обычный Paragraph. Поэтому юридический текст удобно хранить XHTML-шаблоном, а точную финансовую таблицу собирать объектами Table и Cell. Такой смешанный подход уменьшает зависимость от различий с браузером: XHTML отвечает за текстовую разметку, а критичные размеры, подписи и переносы остаются под контролем объектной модели.

Изображения, таблицы стилей и другие относительные ресурсы должны разрешаться из известной базы. В фоновом задании текущая папка может отличаться от среды разработки, поэтому относительный путь без явной основы часто приводит к пропавшей картинке. Надёжнее преобразовать ресурс в абсолютный путь внутри разрешённого каталога, передать содержимое заранее либо использовать обработчик, который сопоставляет допустимые адреса с локальными данными. Одновременно вводят ограничение размера и времени получения, чтобы один внешний ресурс не задержал весь документ.

Визуальную проверку XHTML проводят на граничных данных: длинных словах, вложенных списках, таблице шире области, изображении больше страницы, разрыве перед заголовком и нескольких последовательных формах. Один удачный пример с коротким текстом не подтверждает устойчивость шаблона. Эталонный PDF сохраняют вместе с входным XHTML и набором ресурсов, чтобы изменение CSS или шрифта можно было воспроизвести.

XML и XSL как управляемый шаблонный конвейер

XML-представление полезно, когда структура документа должна храниться отдельно от кода. Приложение готовит данные, XSL преобразует их в элементы объектной модели, а TallPDF.NET строит Document. В этом процессе важно разделить данные и команды макета: пользовательские строки должны попадать в текстовые узлы после корректного экранирования, а названия классов, свойств и обработчиков не должны формироваться из непроверенного ввода.

Шаблон удобнее поддерживать, если общие размеры и стили вынесены в параметры XSL. Поля секции, ширины колонок, шрифты заголовков и оформление денежных ячеек задают в одном месте, а не повторяют в каждом условном блоке. Условия XSL используют для наличия раздела или выбора компактного макета, но сложные расчёты сумм и бизнес-правила лучше выполнять до преобразования. Тогда один и тот же набор данных даёт одинаковые итоги независимо от оформления.

Перед генерацией валидируют исходный XML и результат преобразования. Первая схема проверяет обязательные реквизиты и типы данных, вторая — допустимую структуру документа. Отдельно контролируют пустые коллекции: отсутствие строк заказа может означать таблицу только с заголовком, специальное сообщение или запрет выпуска документа. Такое решение должно быть явным; иначе XSL выдаст формально корректный, но бессодержательный PDF.

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

Для диагностики сохраняют идентификатор шаблона, хеш входного XML и сведения об ошибке, но не обязательно весь документ с персональными данными. Если разрешено хранить тестовые данные, создают обезличенный пакет воспроизведения: XML, XSL, локальные изображения и настройки генерации. Он позволяет повторить сбой без доступа к рабочей базе и сравнить результат после исправления.

События страниц и абзацев в потоковой генерации

Событийная запись уменьшает память за счёт последовательного вывода готовых страниц. Содержимое можно подавать через PrintParagraph, когда движку требуется следующий Paragraph, а PrintFirstParagraph — когда нужна первая строка очередной страницы, например повторяемая шапка реестра. Набор данных читают порциями и создают только текущую строку или небольшой блок. После исчерпания данных обработчик должен явно прекратить выдачу, иначе логика способна повторять последний элемент или ждать продолжения.

QueryPageSettings вызывается перед добавлением страницы и позволяет назначить её размеры и page boxes. StartPage срабатывает после создания страницы и подходит для областей, зависящих от номера листа; EndPage позволяет добавить завершающие элементы до перехода к следующей странице. Эти события полезны, когда формат или фон меняются внутри длинного потока без создания обычных секций. Обработчики не должны выполнять дорогой сетевой запрос для каждой страницы: нужные настройки подготавливают заранее или кэшируют.

На уровне Paragraph доступны события BreakParagraph, ContinueParagraph, EndParagraph, RollbackParagraph и TransformParagraph. Они помогают наблюдать перенос, продолжение и откат из-за KeepWithNext, но требуют осторожности. Обработчик не должен менять уже рассчитанное содержимое непредсказуемым образом или повторно добавлять тот же объект. Для диагностики события можно логировать с номером записи и типом абзаца, а в рабочем режиме оставлять только необходимые операции.

Ограничения событийного режима непосредственно влияют на таблицы. ColSpan и FitToContent не применяются, поэтому ширины задают заранее через PreferredWidth; каждая секция начинает новую страницу; ссылки между абзацами, контекстные поля и автоматическое оглавление не вычисляются. Эти правила проверяют до выбора режима. Реестр с фиксированными колонками подходит хорошо, а документ с динамической таблицей содержания, ссылками см. раздел и объединёнными ячейками требует обычной модели.

Память измеряют вместе с набором данных. Даже если генератор хранит одну страницу, вызов ToList для миллиона записей сохранит весь набор в приложении. Чтение должно быть последовательным, изображения — освобождаться после того, как они больше не нужны, а повторяемые ресурсы — кэшироваться ограниченно. Для нескольких одновременных отчётов вводят лимит параллелизма, потому что суммарные буферы потоков и декодированные изображения могут превысить доступную память.

Формы, кнопки и действия: проверка поведения

Поля формы размещаются как Shape внутри Drawing, то есть их геометрия задаётся координатами. Это удобно для анкеты или бланка, но координаты рассчитывают относительно Drawing, а не потока соседних абзацев. Если над формой добавился длинный текст, вся Drawing переместится как единый Paragraph, а поля сохранят внутреннее расположение. Для наложения на готовую страницу размер Drawing и секции должен совпадать с исходным бланком.

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

GoToAction связывает элемент с Paragraph или страницей, UriAction открывает заданный адрес, JavaScriptAction записывает сценарий PDF. Действия можно присоединять к Fragment, Paragraph, LinkShape, кнопке и событиям Document. Однако их исполнение зависит от программы чтения и настроек безопасности. Критичная операция, например расчёт суммы или проверка обязательных реквизитов, должна выполняться до генерации; сценарий в PDF оставляют для удобства интерфейса, а не как единственный контроль.

Проверку проводят минимум в двух целевых просмотрщиках, если документ распространяется широко. Нужно убедиться, что табуляция проходит поля в ожидаемом порядке, ссылки активны, кнопка не перекрыта передним слоем, кириллица вводится выбранным шрифтом, а печатная версия сохраняет значения. Браузерный просмотрщик может ограничивать JavaScript и отдельные свойства формы, поэтому успешный тест в одном клиенте не гарантирует одинаковое поведение в другом.

Архитектура генератора для пакетных и фоновых задач

Практичный генератор разделяют на получение данных, нормализацию, построение Document и запись результата. Слой данных не должен создавать Paragraph напрямую: сначала формируют типизированную модель отчёта с уже рассчитанными суммами, датами и строковыми представлениями. Затем шаблон переводит её в секции, таблицы и рисунки. Такое разделение позволяет тестировать бизнес-правила без PDF и менять оформление без запросов к базе.

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

В фоновой очереди каждому заданию назначают идентификатор, предельное время и максимальный размер результата. Выход сначала пишут в MemoryStream только для умеренных файлов; большой отчёт направляют в файловый или облачный поток, чтобы не держать две копии в памяти. После успешного Write поток закрывают, проверяют размер и сигнатуру, затем атомарно публикуют объект. Пользователь не должен получить частично записанный PDF, если генерация оборвалась.

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

Параллельность ограничивают на уровне очереди. Создание нескольких независимых Document допустимо, но общие изменяемые коллекции, потоки и графические ресурсы нельзя передавать между заданиями без синхронизации. Оптимальный лимит определяют нагрузочным тестом с реальными изображениями и таблицами: короткий текстовый PDF почти ничего не говорит о пиковом потреблении памяти.

Итерационная настройка макета без визуального редактора

Макет удобнее разрабатывать от крупных блоков к мелким. Сначала выводят рамки секции, колонтитулов, таблиц и Drawing контрастными Pen, чтобы увидеть фактические границы. Затем фиксируют размеры и переносы, после чего заменяют диагностическое оформление финальным. Попытка одновременно подбирать шрифт, фон, координаты и бизнес-условия усложняет поиск причины любого сдвига.

Для каждого сложного блока создают отдельный небольшой документ. Таблицу проверяют без остального отчёта на десяти наборах ширины; форму — на пустом листе и на подложке; колонтитул — на чётной, нечётной и первой странице; XHTML — с локальным набором ресурсов. После стабилизации блок включают в общий шаблон. Этот подход быстрее, чем каждый раз формировать сотни страниц ради проверки одной ячейки.

Диагностический режим можно включать параметром шаблона. Он добавляет границы Cell и Area, подписи координат Drawing, номера записей и маркеры начала секций. В готовом документе эти элементы выключаются, но тот же вход легко воспроизвести с визуальными подсказками. Диагностический режим не должен менять размеры объектов; иначе ошибка исчезнет или появится только из-за дополнительного текста.

Изменения проверяют на эталонном наборе, а не на одном красивом счёте. Нужны минимальная и максимальная длина адреса, нулевая и большая таблица, отсутствие логотипа, изображение необычного соотношения сторон, несколько валют, перенос секции и длинный комментарий. Результаты растрируют одинаковым способом и сравнивают страницы, а структурные тесты отдельно проверяют число страниц, закладки, поля и защиту.

Тестирование созданных PDF

Первый уровень проверки — сигнатура файла, ненулевой размер и отсутствие исключения. Этого недостаточно: PDF может открываться, но иметь обрезанную таблицу или пропущенный шрифт. Для каждого шаблона создают контрольные наборы: пустой, обычный, максимально длинный, с отсутствующим изображением, с несколькими страницами и с символами всех языков.

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

Тесты для таблиц должны включать строку выше страницы, длинное неразрывное слово, ColSpan и перенос между страницами. Для событийного режима поддерживают отдельный набор, поскольку ограничения отличаются. XHTML проверяют на всех используемых CSS-конструкциях; добавление нового свойства в шаблон сопровождают тестовым PDF, а не только просмотром HTML.

Оптимизация размера и скорости

Большую часть объёма обычно дают изображения и шрифты. Фотографии заранее уменьшают до реального печатного размера и разумного качества JPEG, схемы и логотипы оставляют в подходящем формате без лишнего разрешения. Повторное кодирование уже сжатого JPEG в PNG увеличивает файл без улучшения. Встраивание полного шрифта вместо подмножества также заметно влияет на размер.

Скорость измеряют на реальных документах отдельно для подготовки данных, построения модели и Write. Если основное время уходит на сетевые изображения, оптимизация таблицы не поможет. Ресурсы кэшируют с ограничением объёма, а обращения к базе выполняют пакетно. Для независимых документов допустима параллельная генерация, но один Document не следует менять одновременно из нескольких потоков без явной гарантии API.

Событийный режим выбирают для длинного линейного потока, где заранее известны ширины колонок и не нужны перекрёстные ссылки. Для сложного оглавления и итоговых полей обычный режим проще и функциональнее. Оптимизация должна сохранять предсказуемый макет; экономия нескольких мегабайт не оправдывает исчезновение заголовков или неверные номера страниц.

Сравнение TallPDF.NET с аналогами

ПрограммаЛучше подходит дляГлавное ограничение
TallPDF.NETОтчёты .NET с потоковым макетом, XML/XSL, XHTML, формами и точным рисованиемМакет проектируется через код и объектную модель
QuestPDFНовые C#-проекты с fluent-разметкой и удобной разработкой шаблоновУсловия бесплатной лицензии зависят от типа и масштаба организации
iText 7Создание и глубокая обработка PDF, стандарты, подписи и сложные корпоративные процессыAGPL требует соблюдения copyleft либо коммерческой лицензии
MigraDoc / PDFsharpДокументная модель .NET, отчёты и базовые операции с PDF в проектах с открытым исходным кодомНет штатного полноценного преобразования HTML в PDF
Aspose.PDF for .NETШирокая конвертация, создание и изменение существующих PDF одним коммерческим APIОценочный режим ограничивает число обрабатываемых страниц

TallPDF.NET рационально выбирать, когда уже используется его объектная модель, нужны XML-шаблоны, контекстные поля или событийная выдача очень длинных отчётов. QuestPDF удобнее для нового code-first макета с fluent API. iText 7 подходит, когда генерация сочетается с глубокой обработкой, стандартами и подписями, но лицензионную модель нужно согласовать заранее. MigraDoc/PDFsharp уместен для классических отчётов без полноценного HTML-конвейера. Aspose.PDF выбирают, когда один API должен не только создавать, но и активно конвертировать и редактировать существующие документы.

Как выбрать способ построения макета

Объектная модель C# лучше всего подходит для документов, тесно связанных с бизнес-логикой: состав колонок меняется по данным, блоки добавляются условно, изображения приходят потоками, а ошибки нужно обрабатывать типизированным кодом. XML/XSL удобнее, когда структура хранится отдельно, редактируется как шаблон и формируется из существующей XML-модели. XHTML полезен для контролируемой разметки, уже описанной таблицами и CSS 2.1, но не для копирования произвольного сайта.

Flow-объекты выбирают для текста, списков, таблиц и изображений, которые должны переноситься автоматически. Drawing используют для фиксированных координат, подложек, штрихкодов и форм. Area решает задачу повторения элемента на страницах. Смешанный макет обычно надёжнее, чем попытка рисовать весь отчёт вручную или, наоборот, заставить поток совпасть с заранее напечатанными клетками.

Обычный Write даёт перекрёстные ссылки и контекстные поля. Событийный режим уменьшает память, но требует фиксированных ширин и отказа от части функций. Этот выбор фиксируют в техническом задании до разработки таблиц и оглавления.

Контрольный список перед выпуском

  • Проверить PageSize, ориентацию, поля и высоту колонтитулов на каждой секции.
  • Прогнать короткие и максимальные значения через все таблицы, включая неразрывные идентификаторы.
  • Убедиться, что шрифт содержит кириллицу и специальные символы и доступен в среде публикации.
  • Ограничить размеры изображений и время загрузки внешних ресурсов.
  • Проверить RepeatFirstRow, Row.DoNotBreak, KeepWithNext и отсутствие пустой последней страницы.
  • Открыть формы, ссылки и JavaScript в целевых PDF-просмотрщиках.
  • Сравнить обычный и событийный режим с требованиями к оглавлению, ColSpan и общему числу страниц.
  • Проверить защиту, метаданные, закладки и текстовый слой автоматическими тестами.

После этих проверок шаблон можно использовать в пакетной обработке и фоновых заданиях. При изменении данных, шрифтов или среды публикации контрольный PDF создают заново: макет зависит не только от кода, но и от фактических ресурсов, доступной ширины и поведения выбранного режима генерации.

Ответы на практические вопросы

Можно ли собрать PDF полностью без координат?

Да. TextParagraph, Table, Image, Heading и другие Paragraph автоматически перетекают по секции и создают страницы. Координаты нужны только внутри Drawing, Area и фигур, когда требуется точное размещение.

Как добавить страницу из готового PDF?

Страницу помещают как PageShape в Drawing. Для исходного размера секции назначают те же габариты, поля обнуляют, а Drawing растягивают на всю страницу. Для сложного редактирования готового файла нужен API, ориентированный на чтение и изменение PDF.

Почему номер общего количества страниц не работает в потоковом режиме?

Поле #P требует знать финальную пагинацию. Событийная генерация сбрасывает готовые страницы и не хранит весь документ для разрешения контекстных значений. Используют обычный режим или отказываются от общего счётчика.

Можно ли вставить обычную HTML-страницу?

XhtmlParagraph ожидает корректный XHTML и поддерживаемое подмножество CSS. Браузерный JavaScript и динамический DOM не выполняются, а результат может отличаться от браузера. Надёжнее применять специально подготовленный шаблон.

Как сохранить документ без временного файла?

Write принимает Stream. Для веб-ответа, письма или хранилища используют MemoryStream либо поток назначения. После записи позицию возвращают к началу, если тот же поток будет читаться.

Что делать с очень большой таблицей?

Сначала фиксируют ширины, повторяют строку заголовка и проверяют длинные значения. Затем рассматривают событийный режим и порционное чтение данных. Если нужны ColSpan, FitToContent и перекрёстные ссылки, остаются в обычном режиме и контролируют память набора данных.

Как избежать наложения водяного знака на поля формы?

Выбирают BackgroundArea, если знак должен быть под содержимым, или ForegroundArea для штампа поверх него. Затем проверяют порядок областей, Opacity и координаты. Интерактивность поля зависит и от PDF-просмотрщика, поэтому нужен тест целевого клиента.

Можно ли переиспользовать фирменный блок в разных документах?

Да. Повторяемый потоковый блок оформляют наследником Paragraph или Heading, координатный — наследником ShapeCollection. Для XML сохраняют стабильное полное имя класса и значения по умолчанию.

Итоговый рабочий подход

Надёжный шаблон TallPDF.NET строится слоями. Сначала задают секции, формат бумаги и повторяемые области. Затем добавляют потоковые абзацы и таблицы, добиваясь корректных переносов на граничных данных. После этого подключают изображения, Drawing, формы, действия и контекстные поля. В конце выбирают режим Write, проверяют ресурсы в среде публикации и автоматизируют визуальную и структурную проверку PDF.

Главная причина проблем — попытка решить задачу не тем уровнем модели. Пробелы не заменяют Spacing, пустые строки не заменяют разрыв, ForceWidth не преодолевает минимальную ширину, браузерный шаблон не становится корректным XHTML автоматически, а событийный режим не может вычислить сведения, требующие знания будущих страниц. Когда свойства назначаются на подходящем объекте, макет остаётся устойчивым при изменении текста и числа записей.

Для регулярной отчётности стоит создать набор собственных компонентов: заголовки, денежные ячейки, колонтитул, блок реквизитов, обработчик изображений и валидатор входного XML. Тогда новый документ собирается из проверенных частей, а различия сводятся к данным и нескольким параметрам секции. Такой подход уменьшает число визуальных дефектов и делает результат воспроизводимым в пакетных заданиях.