PDFHummus позволяет из кода C++ создавать PDF с нужным размером страниц, размещать текст, шрифты, изображения и векторные фигуры, объединять документы, дописывать содержимое в существующие страницы и разбирать внутренние объекты файла. Основные инструменты — PDFWriter, контексты содержимого страниц, Form XObject, PDFModifiedPage и PDFParser; вместе они дают контроль над координатами, ресурсами, потоками, шифрованием и структурой итогового документа.
Работа строится вокруг явного жизненного цикла документа: код открывает выходной поток, создаёт страницу, получает контекст её содержимого, записывает PDF-операторы и завершает страницу до закрытия файла. Такой порядок избавляет от скрытой разметки, но требует аккуратно управлять объектами, проверять EStatusCode и завершать каждый начатый контекст. Для типовых задач доступны высокоуровневые вызовы размещения текста и изображений, а для нестандартной графики можно перейти к непосредственным операторам PDF.
Практическая ценность библиотеки особенно заметна в генераторах счетов, отчётов, этикеток и персонализированных документов, где макет формируется данными и должен воспроизводиться одинаково при каждом запуске. Она также подходит для пакетного объединения страниц, нанесения штампов, внедрения готовых PDF как повторно используемой графики и анализа словарей, массивов, потоков и косвенных ссылок. При этом визуального редактора нет: расположение элементов задаётся координатами и кодом, а результат проверяется в программе просмотра PDF.
Скачать PDFHummus
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет графического интерфейса
- Нужны C++ и CMake
- Нет визуальной разметки
Как устроен рабочий процесс
PDFWriter служит центральной точкой записи. В начале операции ему передают путь или выходной поток, выбранный уровень PDF и параметры создания. После успешного StartPDF можно создавать страницы и вспомогательные объекты. Каждая страница получает прямоугольник MediaBox, который определяет её физический размер в пунктах. Для A4 обычно используют 595 на 842 пункта, а для американского Letter — 612 на 792. Координаты не зависят от разрешения экрана: один пункт равен одной семьдесят второй дюйма, поэтому печатный размер предсказуем.
Содержимое страницы записывается через PageContentContext. Контекст не является холстом с автоматической компоновкой: он формирует последовательность операторов внутри content stream. Текст, линия, изображение и трансформация появляются именно в том порядке, в котором вызваны методы. Если сначала нарисовать фон, затем фотографию и в конце подпись, подпись окажется сверху. Если порядок перепутать, поздний непрозрачный объект перекроет ранний, и исправлять это придётся изменением последовательности команд.
После наполнения страницы код передаёт её PDFWriter методом WritePageAndRelease или эквивалентным способом завершает запись и освобождает объект. В конце EndPDF дописывает каталог, дерево страниц, таблицу перекрёстных ссылок и trailer. Пропуск финализации часто даёт файл нулевой длины, обрезанный поток либо документ, который просмотрщик пытается восстановить. Поэтому проверка кода возврата после StartPDF, записи каждой страницы и EndPDF важнее, чем простая проверка существования файла.
Минимальный каркас документа
PDFWriter writer;
EStatusCode status = writer.StartPDF("result.pdf", ePDFVersion17);
if (status != eSuccess) return 1;
PDFPage* page = new PDFPage();
page->SetMediaBox(PDFRectangle(0, 0, 595, 842));
PageContentContext* ctx = writer.StartPageContentContext(page);
// Текст, изображения и графика добавляются через ctx.
writer.EndPageContentContext(ctx);
status = writer.WritePageAndRelease(page);
if (status != eSuccess) return 2;
return writer.EndPDF() == eSuccess ? 0 : 3;
Этот каркас полезно вынести в небольшой RAII-слой проекта. Обёртка может гарантировать вызов EndPageContentContext и освобождение PDFPage даже при исключении, а также переводить EStatusCode в понятное сообщение журнала. Однако сама библиотека использует явные указатели и статусы, поэтому нельзя предполагать, что незавершённый объект автоматически попадёт в файл при выходе из области видимости.
Сборка и подключение к проекту
Для стандартной сборки нужны компилятор C++, CMake и зависимости, отвечающие за шрифты, сжатие и изображения. В Windows обычно применяется Visual Studio, в Linux и macOS — GCC или Clang. Из каталога исходников создают отдельную папку build, выполняют конфигурирование, затем сборку Release. Отдельный каталог сборки удобен тем, что исходное дерево остаётся чистым, а параметры Debug и Release можно хранить раздельно.
mkdir build
cd build
cmake ..
cmake --build . --config Release
ctest --test-dir . -C Release
cmake --install . --prefix ./install --config Release
Установка в собственный prefix формирует структуру заголовков и библиотек, которую проще подключать к нескольким приложениям. При интеграции через CMake целевой объект связывают с PDFHummus::PDFWriter. Такой target переносит пути include и необходимые параметры линковки, поэтому ручное перечисление десятков заголовков менее надёжно. Другой вариант — FetchContent: проект получает исходники при конфигурировании и собирает их вместе со своим кодом. Для воспроизводимости в GIT_TAG следует закреплять конкретный тег, а не плавающую ветку.
По умолчанию используются вложенные копии FreeType, LibAesgm, LibJpeg, LibPng, LibTiff и Zlib. Параметр USE_BUNDLED позволяет отказаться от них и связаться с библиотеками системы. Это полезно для дистрибутивов Linux, где политика безопасности требует единого обновляемого экземпляра каждой зависимости. Обратная сторона — различия версий и путей поиска. Режим USE_UNBUNDLED_FALLBACK_BUNDLED позволяет сначала искать системные пакеты, а при отсутствии брать вложенные.
- PDFHUMMUS_NO_DCT отключает декодирование JPEG и связь с LibJpeg.
- PDFHUMMUS_NO_TIFF исключает поддержку TIFF и зависимость LibTiff.
- PDFHUMMUS_NO_PNG исключает PNG и зависимость LibPng.
- PDFHUMMUS_NO_OPENSSL убирает функции шифрования PDF 2.0 и связь с OpenSSL.
- BUILD_FUZZING_HARNESS включает цель для проверки парсера случайными и повреждёнными данными.
Отключать компонент стоит только после анализа входных данных. Например, генератор, который пишет лишь векторные счета и использует JPEG без декодирования, может не нуждаться во всех обработчиках, но модуль, принимающий документы клиентов, должен учитывать PNG, TIFF и зашифрованные PDF. Ошибка конфигурации проявляется не при запуске пустого примера, а на первом файле соответствующего типа, поэтому набор тестов должен содержать реальные образцы каждого разрешённого формата.
Создание страниц и выбор системы координат
В PDF начало пользовательской системы координат обычно находится в левом нижнем углу MediaBox. Ось X растёт вправо, ось Y — вверх. Это отличается от многих экранных интерфейсов, где Y увеличивается вниз. Чтобы поставить объект на расстоянии 20 мм от верхнего края страницы высотой H, его нижнюю координату рассчитывают как H минус верхний отступ минус высота объекта. Перевод миллиметров в пункты выполняется по формуле мм × 72 / 25,4.
MediaBox описывает полную страницу, CropBox — видимую область, а другие рамки могут задавать зону обрезки и печати. При создании простого документа достаточно MediaBox, но при копировании страниц важно не считать, что левый нижний угол всегда равен нулю. Встречаются документы с отрицательными координатами и повернутыми страницами. Для наложения штампа безопаснее получить фактический прямоугольник и параметр Rotate из исходной страницы, затем построить матрицу с учётом этих значений.
Матрица преобразования задаётся шестью числами a, b, c, d, e, f. Пара a и d обычно отвечает за масштаб, b и c — за поворот или наклон, e и f — за перенос. Оператор cm умножает текущую матрицу, поэтому несколько преобразований накапливаются. Практическое правило: перед локальным переносом вызвать q, выполнить cm, нарисовать объект и затем вызвать Q. Так масштаб или поворот логотипа не изменит координаты следующего текста.

На показанном результате заголовок и растровое изображение располагаются в одной координатной системе. Такой макет не требует предварительного создания картинки всей страницы: фотография остаётся отдельным ресурсом, а надпись — текстом. Это сохраняет качество печати и позволяет копировать текст, если выбранный шрифт и кодировка сформированы корректно.
Векторная графика и графическое состояние
PageContentContext предоставляет методы, соответствующие базовым операторам PDF. m переносит текущую точку, l добавляет отрезок, c строит кубическую кривую Безье, re создаёт прямоугольный путь. Путь сам по себе невидим: его нужно обвести S или s, залить f, либо совместить заливку и обводку. Толщина линии, тип соединения, окончания и штриховой шаблон входят в графическое состояние и действуют на последующие пути, пока не изменены или не восстановлены оператором Q.
Цвет задаётся отдельно для обводки и заливки. В PDF доступны серый, RGB и CMYK; выбор должен соответствовать назначению документа. Для экранного отчёта проще RGB, для полиграфии могут потребоваться CMYK-значения, согласованные с процессом печати. Библиотека записывает числовые компоненты цвета, но не выполняет полноценное управление ICC-профилями автоматически. Если заказчик требует PDF/X или конкретный OutputIntent, соответствующие словари и профили придётся добавлять осознанно.
Отсечение создаёт путь, превращает его в clipping path и ограничивает последующую графику. Это удобно для круглой фотографии, полосы, выходящей за рамку, или миниатюры страницы внутри фиксированного окна. Отсечение также входит в графическое состояние, поэтому его почти всегда заключают между q и Q. Забытый Q — типичная причина, по которой всё содержимое ниже внезапно исчезает: оно продолжает рисоваться, но остаётся за пределами узкой области clipping path.

Фигуры в примере демонстрируют, что обводка и заливка независимы: один объект может иметь только контур, другой — сплошную заливку, а третий — оба свойства. Для диаграмм, штампов и декоративных элементов это эффективнее растрового изображения, поскольку контуры масштабируются без пикселизации и обычно занимают мало места.
Текст, шрифты и измерение строк
Шрифт получают через PDFWriter по пути к файлу. Поддерживаются TrueType, OpenType с контурами TrueType или CFF, Type 1 в паре PFB/PFM, DFont и коллекции TTC. Для коллекции важно выбрать нужный индекс лица. Форматы Type 1 CID и Multiple Master не относятся к обычному поддерживаемому пути. Перед массовой генерацией стоит проверить конкретные шрифты на кириллице, цифрах, знаках валют и символах, которые появляются в данных, а не ограничиваться латинским тестом.
Высокоуровневая запись текста принимает объект используемого шрифта, размер и параметры цвета. Строки передаются в UTF-8, после чего библиотека сопоставляет символы с глифами и формирует ресурсы шрифта. Если символа в выбранном файле нет, его нельзя получить одной лишь сменой кодировки: понадобится шрифт с нужным глифом или разбиение строки на фрагменты с fallback-шрифтами. Набор fallback лучше определять заранее по диапазонам Unicode, чтобы не обнаружить пустые квадраты в фамилиях или адресах после выпуска тысяч документов.
CalculateTextDimensions помогает узнать ширину и высоту фрагмента при конкретном шрифте и размере. На основе этой величины можно выровнять подпись по правому краю, центрировать заголовок или решить, помещается ли строка в ячейку. Перенос по словам всё равно реализует вызывающий код: он накапливает слова, измеряет кандидатную строку и создаёт новую строку при превышении ширины. Для точного интерлиньяжа учитывают метрики шрифта и выбранный leading, а не только размер кегля.

На демонстрационной странице видны несколько режимов: масштабирование текста по горизонтали, межсимвольный интервал, многострочная запись, рендеринг контура и прямое размещение глифов. Для обычного абзаца достаточно методов записи строки, но логотипные надписи, выравнивание по глифам и специальные эффекты удобнее строить через текстовые операторы BT, ET, Tf, Tm, Tj и TJ. При таком подходе разработчик сам отвечает за корректную последовательность и сброс состояния.
Практика работы с кириллицей
Кириллический текст проверяют в трёх режимах: визуальное отображение, копирование в буфер и поиск в просмотрщике. Документ может выглядеть правильно, но содержать неполную карту ToUnicode, из-за чего поиск и извлечение дадут неправильные символы. Если PDF используется для архива, доступности или дальнейшего анализа, тест копирования обязателен. Для счетов и форм дополнительно проверяют неразрывный пробел, длинное тире, знак рубля, дробные числа и смешанные латинско-кириллические коды.
Не следует создавать новый PDFUsedFont для каждой строки. Повторное использование одного объекта шрифта позволяет библиотеке вести общий набор задействованных глифов и уменьшает число ресурсов. Если документ содержит сотни страниц, полезно кэшировать шрифты по абсолютному пути, индексу лица и параметрам. При этом кэш живёт в рамках одного PDFWriter: перенос внутреннего объекта шрифта между двумя одновременно открытыми документами без явной поддержки приведёт к неверным ссылкам на объекты.
Изображения JPG, PNG и TIFF
Для размещения изображения высокоуровневый метод DrawImage принимает координаты, путь или поток и ImageOptions. JPEG часто встраивается без полного пересжатия, что сохраняет качество и ускоряет генерацию. PNG обрабатывает прозрачность, а TIFF может содержать несколько страниц и разные схемы сжатия. Перед размещением можно запросить размеры через GetImageDimensions и рассчитать масштаб без пробного рисования.
ImageOptions позволяет задать матрицу напрямую либо использовать режим подгонки. При eFit изображение помещается в прямоугольник назначения; параметр сохранения пропорций предотвращает растяжение. Выбор между fit и crop следует сделать явно. Fit показывает весь кадр и оставляет свободные поля, если пропорции различаются. Crop заполняет рамку, но часть изображения выходит за clipping path. Для фотографии сотрудника обычно нужен crop, а для схемы или скана — fit, чтобы не потерять данные по краям.
Одно и то же изображение на нескольких страницах желательно превращать в повторно используемый ресурс либо доверять механизму кэширования в рамках документа, если выбранный вызов его использует. Повторное включение байтов логотипа в каждый content stream резко увеличит файл. После генерации это заметно по размеру: десять страниц с фотографией не должны занимать примерно в десять раз больше одной страницы, если изображение действительно переиспользуется.

TIFF требует особого внимания, потому что контейнер допускает многостраничность, палитры, оттенки серого, bilevel-данные и разные виды компрессии. Индекс изображения в ImageOptions выбирает нужную страницу. Перед циклом полезно определить число IFD или обрабатывать индексы до ошибки окончания. Не следует считать TIFF обычной фотографией: технические чертежи в 1 bit могут лучше храниться как маска, а цветные сканы — как полноценный image XObject.


Если изображение не появляется, сначала проверяют доступность файла и статус вызова, затем формат и конкретный индекс страницы. Следующий шаг — открыть журнал и убедиться, что декодер не отключён параметром сборки. Для прозрачного PNG проверяют, не лежит ли объект под непрозрачным фоном. Для очень большого TIFF оценивают память: декодирование многомегапиксельной страницы может потребовать значительно больше исходного размера, особенно после преобразования в RGB.
Form XObject и повторное использование графики
Form XObject — самостоятельный поток графических команд с собственным прямоугольником и ресурсами. Его создают один раз, наполняют через XObjectContentContext, завершают и регистрируют в ресурсах страницы. Затем оператор Do выводит форму любое число раз. Перед Do можно задать cm, поэтому один шаблон штампа легко разместить в разных координатах, повернуть и масштабировать без дублирования описания фигур и текста.
PDFFormXObject* form = writer.StartFormXObject(PDFRectangle(0, 0, 200, 100));
ObjectIDType id = form->GetObjectID();
XObjectContentContext* fctx = form->GetContentContext();
fctx->q();
fctx->re(0, 0, 200, 100);
fctx->S();
fctx->Q();
writer.EndFormXObjectAndRelease(form);
std::string name = page->GetResourcesDictionary().AddFormXObjectMapping(id);
ctx->q();
ctx->cm(1, 0, 0, 1, 200, 600);
ctx->Do(name);
ctx->Q();
Регистрация в словаре Resources обязательна: content stream ссылается не на номер объекта напрямую, а на имя вроде /X1. Метод AddFormXObjectMapping связывает это имя с объектом формы. Если скопировать только оператор Do без соответствующей записи в Resources, просмотрщик сообщит об отсутствующем XObject или просто ничего не покажет. Аналогичное правило действует для шрифтов и изображений.
Формы особенно полезны для логотипов, водяных знаков, рамок, повторяющихся элементов бланка и миниатюр других PDF-страниц. Они также упрощают z-order: форму можно подготовить заранее, а вывести именно в нужном месте последовательности. Ограничивающий BBox формы не всегда автоматически обрезает всё содержимое так, как ожидает разработчик, поэтому выходящую за границы графику лучше дополнительно контролировать clipping path.
Объединение документов и встраивание страниц
Для простого присоединения используется операция копирования диапазона страниц из другого PDF. Диапазон задаёт номера исходных страниц и порядок. Можно взять весь документ, отдельные страницы или несколько интервалов, а затем продолжить запись собственных страниц. В результате библиотека переносит объекты, на которые ссылаются выбранные страницы: шрифты, изображения, формы и другие ресурсы. Это надёжнее, чем растрировать каждую страницу, потому что текст и векторная графика сохраняются.
Когда исходную страницу нужно не добавить целиком, а поместить внутрь новой страницы, её преобразуют в Form XObject. Так строят листы с несколькими слайдами, предпросмотр вложений, монтаж визиток и схемы два оригинала на одном листе. После создания формы её матрицей вписывают в выбранный прямоугольник. Расчёт должен учитывать CropBox, Rotate и исходную систему координат, иначе миниатюра окажется повёрнутой, смещённой или обрезанной.



PDFDocumentCopyingContext даёт более низкоуровневый контроль. Через него можно копировать конкретные объекты и строить собственную структуру назначения. Такой путь нужен при выборочном переносе ресурсов, сложном интерливинге нескольких документов или создании формы из исходной страницы. Но вместе с гибкостью приходит обязанность отслеживать отображение номеров объектов: один и тот же номер в двух входных файлах не означает один и тот же объект, поэтому нельзя переносить ссылки обычным копированием чисел.



При слиянии документов проверяют закладки, формы AcroForm, аннотации и именованные назначения. Перенос страницы ещё не гарантирует, что внешняя навигация сохранит прежний смысл: ссылка могла вести на страницу, которая не вошла в диапазон. Для пакета договоров часто достаточно визуального содержимого, но для интерактивного каталога необходим отдельный тест переходов, полей и действий.
Добавление содержимого в существующий PDF
Режим модификации открывает исходный документ и записывает обновлённый результат. Для выбранной страницы создаётся PDFModifiedPage, после чего новый PageContentContext формирует дополнительный поток. Этот поток можно разместить поверх исходного содержимого, поэтому сценарии штампа, номера страницы, даты, QR-кода и подписи не требуют полного разбора прежних операторов. Исходная графика остаётся в своих потоках, а новая добавляется отдельным слоем.
Наложение поверх не равно редактированию существующего текста. Если закрыть старую надпись белым прямоугольником и написать новую, исходные символы останутся в файле и могут быть извлечены. Такой приём не подходит для удаления конфиденциальных данных. Настоящее удаление требует найти операторы, удалить или изменить их, обработать перекрывающиеся ресурсы и убедиться, что старые байты не сохраняются в предыдущей ревизии. Инкрементальная структура PDF делает эту задачу особенно сложной.
При записи в тот же путь нельзя одновременно читать исходный файл и обрезать его на выходе. Безопасный алгоритм создаёт временный результат рядом, завершает и проверяет его, затем атомарно заменяет оригинал. Если API допускает отдельные input и output, выбирают разные пути. Это защищает документ от потери при аварии, нехватке места или ошибке в середине записи.
Через GetModifiedFileParser можно обращаться к парсеру открытого документа и получать геометрию страниц, словари и ресурсы до наложения. Это позволяет поставить штамп относительно CropBox, прочитать Rotate, проверить наличие аннотаций и определить число страниц. Данные парсера действительны, пока жив соответствующий контекст модификации; сохранять внутренние указатели после закрытия документа нельзя.
Удаление и замена объектов
Низкоуровневый интерфейс позволяет пометить объект как удалённый или записать его обновлённую версию. Пометка объекта не означает рекурсивное удаление всех зависимостей и не гарантирует физического исчезновения старых байтов. Если задача состоит в уменьшении файла, обезличивании или полной очистке метаданных, нужен полный переписывающий процесс с анализом достижимости объектов. Простая инкрементальная запись предназначена прежде всего для корректного обновления структуры, а не для санитарного уничтожения прежнего содержимого.
Разбор структуры через PDFParser
PDFParser можно использовать независимо от записи. После открытия входного потока StartPDFParsing читает служебную структуру, находит цепочку xref, trailer и дерево страниц. Сами объекты загружаются по запросу, что снижает расход памяти на больших документах. Такое ленивое чтение удобно, когда нужны только количество страниц, MediaBox и несколько записей Info, а не полное содержимое каждого потока.
Объекты PDF имеют типы null, boolean, integer, real, name, string, literal hex string, array, dictionary, stream и indirect object reference. Запрашивающий код проверяет тип прежде, чем приводить указатель к конкретному классу. Косвенная ссылка не разворачивается автоматически во всех местах: сначала получают номер объекта, затем запрашивают объект у парсера. Игнорирование этого шага приводит к попытке прочитать словарь там, где находится только ссылка.
Страница в дереве Pages наследует некоторые свойства от родителей, включая MediaBox, CropBox, Rotate и Resources. Поэтому чтение только собственного словаря страницы может вернуть отсутствие ключа, хотя значение существует. Для практических задач используют методы, которые проходят цепочку Parent, либо реализуют безопасный подъём с ограничением глубины и проверкой циклов. Повреждённый файл может содержать циклические ссылки; бесконечный проход нельзя допускать на входных данных от пользователя.
Content stream может быть одиночным потоком или массивом потоков. Перед анализом текста или графики нужно обработать оба случая и применить фильтры декодирования. Затем токены интерпретируются в контексте графического состояния, ресурсов и текущей текстовой матрицы. Простого поиска строковых литералов недостаточно: строка может быть закодирована глифами, разбита оператором TJ, находиться в Form XObject или использовать шрифт без прямого соответствия Unicode.
Извлечение прикладных данных
Для считывания форм сначала находят AcroForm в каталоге, затем обходят Fields и дочерние Kids. Отображаемое поле и логическое поле могут быть разными объектами, а значение наследоваться. Для текста на странице требуется интерпретатор операторов, который поддерживает шрифты, матрицы и вложенные формы. PDFParser предоставляет строительные блоки, но не превращает произвольную страницу в готовую семантическую таблицу одной командой. Поэтому для извлечения счетов обычно добавляют собственный слой распознавания координат и структуры.
Потоки и работа без промежуточного файла
Вместо имени файла PDFWriter может писать в реализацию IByteWriter. Это полезно для серверного ответа, памяти, зашифрованного контейнера или пользовательского хранилища. Поток должен корректно сообщать результат записи и сохранять порядок байтов. Если выход передаётся по сети, следует учитывать, что окончательные xref и trailer появляются только после завершения документа; HTTP-ответ нельзя считать успешным до проверки EndPDF.
Входные PDF и изображения также могут поступать из пользовательских потоков. Такой подход избавляет от временных файлов при чтении из базы данных или объектного хранилища. Но поток должен поддерживать операции, которые ожидает конкретный парсер, включая позиционирование, если оно требуется. Однонаправленный сетевой поток без seek обычно сначала буферизуют либо реализуют поверх кэша блоков.
Владение объектом потока нужно зафиксировать в архитектуре. Если библиотека не принимает владение, вызывающий код обязан держать поток живым до завершения записи или разбора. Если адаптер содержит указатель на std::vector, перемещение или изменение вектора может инвалидировать данные. На практике интерфейс оборачивают классом, который одновременно хранит буфер и адаптер, а разрушение допускает только после закрытия PDFWriter или PDFParser.
Для крупных документов буфер всего результата в памяти невыгоден. Потоковая запись позволяет выпускать объекты последовательно и не хранить модель документа целиком. Однако некоторые прикладные операции — например, вычисление подписи, упаковка в иной контейнер или повторная проверка — могут потребовать доступ к уже записанным байтам. Тогда используют временный файл или seekable-поток, а не бесконечно растущий std::string.
Сжатие, таблица xref и большие файлы
PDFCreationSettings управляет сжатием потоков. Для текстовых и графических content stream сжатие обычно уменьшает размер без потери качества. JPEG уже содержит собственную компрессию, поэтому повторное Flate-сжатие его байтов не даёт существенной выгоды. При отладке временное отключение компрессии облегчает чтение операторов в инспекторе, но рабочий документ лучше выпускать со сжатием, если внешняя спецификация не требует иного.
Классическая таблица xref записывает десятизначные смещения. При очень больших файлах это создаёт ограничение около десяти гигабайт. Параметр WriteXrefAsXrefStream переключает запись на поток перекрёстных ссылок, доступный начиная с PDF 1.5. Одновременно нужно выбрать уровень PDF не ниже 1.5, иначе заголовок будет противоречить структуре. Этот режим полезен для огромных архивов изображений, но большинство бизнес-документов не приближается к пределу.
Даже при потоковой модели размер может расти из-за повторных ресурсов. Диагностика включает подсчёт шрифтов и image XObject, сравнение размеров одинаковых потоков и поиск дубликатов по хэшу до передачи в библиотеку. Если каждый экземпляр логотипа становится новым объектом, стоит создать Form XObject или собственный кэш. Если увеличивается шрифт, проверяют, не создаётся ли новый экземпляр для каждой строки и действительно ли выполняется subset-встраивание.
Большой PDF нужно тестировать не только на открытие первой страницы. Просмотрщик может лениво обнаружить повреждение xref лишь при переходе к концу. Автоматическая проверка должна открыть документ парсером, пройти все страницы, запросить основные словари и декодировать выбранные потоки. Дополнительно сравнивают фактический размер с ожидаемым диапазоном, чтобы заметить внезапное дублирование ресурсов.
Шифрование и OpenSSL
Поддержка шифрования PDF 2.0 связана с OpenSSL. При конфигурировании без OpenSSL соответствующую возможность отключают параметром PDFHUMMUS_NO_OPENSSL, иначе сборка должна найти заголовки и библиотеки подходящей архитектуры. Ошибка вида unresolved external для функций OpenSSL обычно означает, что include найден, а нужная библиотека не связана, либо Debug-приложение пытается использовать несовместимый Release-бинарник.
Шифрование создаваемого документа задаётся через EncryptionOptions в PDFCreationSettings. Пароль владельца, пароль пользователя и разрешения необходимо формировать по политике приложения, а не хранить в исходном коде. Пустой или слабый пароль не превращает PDF в надёжное хранилище секретов. Ограничения печати и копирования также не являются DRM с гарантированной защитой: соблюдение разрешений зависит от просмотрщика.
При открытии зашифрованного PDF парсеру нужен правильный пароль. Модификация такого документа должна сохранять согласованную схему шифрования и корректно зашифровывать новые объекты. Проверка результата включает открытие с пользовательским паролем, отказ при неверном пароле и чтение добавленного содержимого. Если документ участвует в электронной подписи, любое изменение после подписания меняет проверяемую ревизию и должно быть согласовано с процессом подписи.
Ссылки, аннотации и интерактивные элементы
Ссылка в PDF представлена аннотацией с прямоугольником и действием или назначением. При генерации оглавления код вычисляет координаты текста, создаёт область аннотации и связывает её с нужной страницей или URI. Прямоугольник должен соответствовать реальной высоте строки; слишком маленькая зона плохо нажимается, а слишком большая перекрывает соседние элементы. Для внутреннего перехода предпочтительно использовать назначение в документе, а не номер страницы, который изменится при вставке новых листов.
Аннотации не рисуются обычными операторами content stream. Они перечисляются в массиве Annots страницы и имеют собственные словари внешнего вида. Если создать комментарий без appearance stream, разные просмотрщики могут показывать его неодинаково. Для производственного документа формируют внешний вид самостоятельно либо проверяют результат минимум в двух программах просмотра.
Интерактивная форма требует согласования AcroForm, полей и widget-аннотаций. Простое добавление текста поверх пустого поля визуально заполняет бланк, но не изменяет значение поля. Напротив, изменение V без обновления appearance может показать старое значение. Для печатного результата допустимо сплющивание, при котором внешний вид переносится в содержимое страницы, а интерактивные объекты удаляются осознанным переписыванием.
Логирование и обработка ошибок
LogConfiguration задаёт, включать ли журнал, куда его писать и начинать ли файл с BOM. Во время разработки журнал полезно сохранять рядом с результатом и связывать с идентификатором задания. В сервере общий лог нескольких потоков создаёт перемешанные сообщения, поэтому лучше передавать отдельный IByteWriter или синхронизированный приёмник. В журнал не следует без фильтра записывать пароли, содержимое документов и персональные пути.
EStatusCode проверяют сразу после операции, которая может завершиться неудачно. Поздняя проверка EndPDF не всегда объясняет, на какой странице возникла проблема. Обёртка может добавлять контекст: имя входного файла, номер страницы, путь изображения и выполняемый метод. При пакетной обработке стратегия зависит от задачи: для единого договора ошибка одной страницы должна отменить весь результат, а для независимых квитанций можно зарегистрировать сбой и продолжить следующий документ.
- StartPDF не должен считаться успешным только потому, что файл появился на диске.
- Нулевой указатель шрифта требует остановить запись текста, а не продолжать с неопределённым объектом.
- После ошибки декодирования изображения нужно исключить неполный ресурс и не использовать его имя в content stream.
- Каждый вызов q должен иметь соответствующий Q внутри логического блока.
- Каждый начатый контекст страницы или формы должен быть завершён до записи объекта.
- EndPDF должен выполняться один раз после успешного завершения всех страниц.
Для повторяемых ошибок полезен минимальный воспроизводимый документ: одна страница, один шрифт или одно изображение. Он отделяет проблему данных от сложного макета. Если ошибка возникает только на конкретном PDF, вход сохраняют как тестовый fixture после удаления конфиденциальной информации. Парсер и копирующий контекст особенно важно проверять повреждёнными файлами, потому что корректный пример не выявляет циклы, недостающие объекты и неверные длины потоков.
Типовые сценарии применения
Счета, акты и отчёты
Шаблон счёта удобно представить функциями рисования шапки, таблицы позиций и итогов. Размеры колонок фиксируют в пунктах, а высоту строки рассчитывают по переносам текста. Таблица разбивается на страницы до рисования итоговой линии, чтобы строка не оказалась разделена неконтролируемо. Повторяющуюся шапку страницы оформляют Form XObject. Числа форматируют заранее в бизнес-логике; PDFWriter получает уже готовую строку с нужным разделителем и валютой.
Для отчёта с диаграммами графические примитивы подходят для осей, сетки и простых столбцов. Сложный график можно получить из внешней системы как SVG не напрямую, а предварительно преобразовать в поддерживаемое представление или вручную вывести пути. Растровый PNG проще, но при печати мелкие подписи могут потерять резкость. Если диаграмма генерируется собственным кодом, текстовые подписи лучше оставлять текстом PDF.
Штампы, номера и водяные знаки
При нумерации существующего документа парсер определяет число и размеры страниц, затем модификатор добавляет новый поток на каждую страницу. Номер размещают относительно CropBox, а повёрнутые страницы нормализуют матрицей. Полупрозрачный водяной знак требует расширенного графического состояния с opacity; если высокоуровневого вызова недостаточно, соответствующий ресурс и оператор добавляют через низкоуровневый интерфейс. После обработки проверяют страницы разных ориентаций.
Пакетное объединение и монтаж
Для комплекта приложений можно последовательно присоединить выбранные диапазоны, вставляя разделители и собственное оглавление. Сначала формируют план итоговых страниц, чтобы внутренние ссылки получили правильные назначения. При монтаже нескольких страниц на лист создают формы из оригиналов, вычисляют единый коэффициент масштаба и размещают их в сетке. Важный выбор — учитывать CropBox или MediaBox; для печатных меток иногда нужен MediaBox, а для пользовательского содержимого обычно CropBox.
Персонализированная печать
При выпуске тысяч однотипных документов постоянные элементы создают один раз, а переменные текст и штрихкоды добавляют на каждой странице. Шрифты и изображения кэшируют в рамках документа. Входные данные валидируют до начала PDF, чтобы ошибка в одной записи не оставила гигантский незавершённый файл. Если каждый получатель должен получить отдельный документ, параллелизм организуют на уровне независимых PDFWriter, а не совместного доступа к одному объекту.
Практические ограничения
PDFHummus не предоставляет визуального холста, панелей инструментов и интерактивного перемещения объектов. Разработчик задаёт геометрию числами и сам строит перенос строк, таблицы, колонтитулы и правила разрыва страниц. Для пользователя, которому нужно вручную исправить несколько слов или переставить листы мышью, рациональнее редактор PDF. Библиотека оправдана там, где документ формируется программно, повторяется и должен быть встроен в продукт.
Она не является движком HTML/CSS. Нет автоматического flexbox, каскадных стилей, расчёта высоты DOM и переноса сложной таблицы. Можно реализовать собственный layout-слой или воспользоваться сторонним генератором HTML-to-PDF, если макет уже описан веб-технологиями. Преимущество прямого API — предсказуемые координаты и отсутствие браузерного движка; цена — больше прикладного кода.
Парсер предоставляет доступ к структуре PDF, но не гарантирует семантическое восстановление документа. В PDF слово может состоять из отдельных глифов, колонки не имеют явной разметки, а таблица — это набор линий и текстовых фрагментов. Для извлечения нужно сортировать элементы по координатам, учитывать поворот и сопоставлять шрифтовые коды. Скан без текстового слоя потребует OCR, которого API сам по себе не выполняет.
Линеаризованные PDF не относятся к поддерживаемым специальным возможностям записи. Обычный документ открывается после полной загрузки или по стандартным запросам просмотрщика, но режим Fast Web View с оптимизированным порядком объектов не формируется автоматически. Если доставка первого листа до загрузки всего файла критична, результат придётся дополнительно линеаризовать специализированным инструментом и проверить, что последующая операция не разрушила оптимизацию.
Низкоуровневая модификация предполагает знание спецификации PDF. Нельзя безопасно удалить ресурс из словаря только потому, что на текущей странице он кажется неиспользуемым: на него может ссылаться форма. Нельзя менять номер объекта без обновления всех косвенных ссылок. Поэтому сложное переписывание лучше сопровождать структурными тестами и ограничивать заранее определёнными шаблонами входных документов.
Сравнение PDFHummus с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| PDFHummus | Встраиваемой генерации, копирования и разбора PDF в проектах C++ | Макет и управление объектами задаются кодом |
| PDF Commander | Ручного редактирования, объединения и оформления документов без программирования | Не является библиотекой для встраивания в C++ |
| PoDoFo | Разбора, изменения и создания PDF в приложениях C++ с объектной моделью | Для сложной компоновки также нужен собственный код |
| QPDF | Структурных преобразований, проверки, шифрования, разделения и объединения | Не предназначен для полноценной рисующей разметки страниц |
| Apache PDFBox | Создания, изменения и извлечения содержимого в проектах Java | Требует Java-стека и иной модели интеграции |
| libHaru | Компактной генерации PDF из C с базовым текстом и графикой | Слабее подходит для глубокого разбора и изменения готовых PDF |
PDFHummus выбирают, когда основное приложение уже написано на C++ и нужно совместить быструю последовательную запись с доступом к внутренним объектам. PoDoFo ближе всего по языку и классу задач, но его модель и API отличаются, поэтому решение принимают после прототипа на собственных документах. QPDF предпочтителен для сохранных структурных преобразований, где не требуется рисовать сложный макет. PDFBox логичен в Java-сервисе. libHaru подходит для более узкой генерации. PDF Commander лучше, когда задачу выполняет человек и важны визуальные инструменты, а не программная интеграция.
Устранение ошибок сборки и линковки
Заголовок PDFWriter.h не найден
Компилятор должен видеть каталог, внутри которого находится папка заголовков PDFWriter, либо установленный include prefix. В CMake лучше связать target PDFHummus::PDFWriter, чем добавлять относительный путь из рабочей станции. Если исходники скопированы вручную, путь проверяют в фактической команде компилятора с подробным выводом. Ошибка часто возникает, когда IDE настроена, а сборка из командной строки использует другой CMake-профиль.
Неопределённые внешние символы
Такая ошибка означает, что объявление найдено, но реализация не попала в линковку. Проверяют наличие PDFWriter в target_link_libraries, совпадение архитектуры x64 или x86, конфигурации Debug или Release и используемую стандартную библиотеку C++. Затем смотрят зависимости изображений, FreeType, Zlib и OpenSSL. На Unix порядок статических библиотек может иметь значение; импортированный CMake-target обычно решает это лучше ручного списка.
Дублирующиеся символы
Дублирование возникает, когда исходники PDFWriter одновременно компилируются внутри приложения и подключается готовая библиотека, либо системная и вложенная копии одной зависимости попадают в линковку вместе. Нужно выбрать один способ интеграции. При USE_BUNDLED=FALSE убеждаются, что вложенные цели не добавлены другим подкаталогом. В монорепозитории FetchContent_MakeAvailable вызывают один раз, а остальные компоненты ссылаются на уже созданный target.
OpenSSL найден частично
CMake может обнаружить заголовки одной установки и библиотеки другой. Указывают единый root, очищают кэш CMake и конфигурируют заново. Проверяют разрядность и runtime на Windows. Если шифрование PDF 2.0 не нужно, допустимо явно отключить компонент, но это решение фиксируют в требованиях продукта и тестируют реакцию на зашифрованный вход, чтобы система не принимала его молча как обычный файл.
Устранение ошибок при создании PDF
Файл не открывается или восстанавливается просмотрщиком
Первым делом проверяют результат EndPDF и журнал. Затем убеждаются, что каждый PageContentContext завершён, каждая страница записана и освобождена, а форма закрыта до использования. Полезно сравнить размер с минимальным рабочим примером: файл в несколько байтов обычно не получил trailer, а файл большого размера с ошибкой может содержать незавершённый поток. Структурный инспектор покажет отсутствие startxref или несогласованную длину stream.
Текст отсутствует или отображается квадратами
Проверяют, что указатель на шрифт не равен null, файл доступен процессу и содержит нужные глифы. Затем тестируют простую строку тем же шрифтом без трансформаций и clipping. Если латиница работает, а кириллица нет, проблема обычно в покрытии шрифта или преобразовании входной строки в UTF-8. Если текст виден, но не копируется, анализируют ToUnicode и выбранный способ прямого размещения глифов.
Изображение перевёрнуто, растянуто или обрезано
Сначала выводят изображение в исходных пропорциях через размеры, полученные GetImageDimensions. Затем проверяют матрицу: отрицательный масштаб отражает объект, перестановка ширины и высоты поворачивает расчёт. Для JPEG учитывают ориентацию EXIF: не каждый путь интерпретации автоматически применяет её так же, как фотопросмотрщик. Для встроенной PDF-страницы дополнительно учитывают Rotate и ненулевой origin её рамки.
После одного объекта исчезает остальная страница
Ищут несбалансированные q и Q, незакрытый текстовый объект BT/ET и слишком узкий clipping path. Также проверяют цвет: белый текст на белом фоне технически присутствует. Временное отключение сжатия потока и просмотр операторов быстро показывает, какое состояние осталось активным. Каждый компонент макета лучше оборачивать в собственную пару q/Q, чтобы его матрица, толщина линии и clipping не вытекали наружу.
Файл неожиданно вырос
Сравнивают число image XObject и шрифтов на страницах, проверяют повторное использование Form XObject и кэширование ресурсов. Большая фотография, помещённая в маленькую рамку, остаётся большой по количеству пикселей; масштаб в PDF не уменьшает исходные данные. Для веб-квитанции изображение предварительно ресемплируют до разумного разрешения, сохраняя оригинал только там, где нужен качественный печатный вывод.
Устранение ошибок при модификации и разборе
Добавленный штамп смещён на части страниц
Получают фактические MediaBox, CropBox и Rotate каждой страницы, а не используют размер первой страницы для всего документа. Затем переводят желаемую позицию из визуальной системы верхнего левого угла в PDF-координаты. Для поворота 90 или 270 градусов ширина и высота меняются ролями. Тестовый штамп сначала рисуют ярким прямоугольником с известными координатами, после чего заменяют рабочим содержимым.
Парсер возвращает ссылку вместо словаря
Проверяют тип объекта. Если это PDFIndirectObjectReference, извлекают номер и запрашивают целевой объект у PDFParser. После разворачивания снова проверяют тип, потому что повреждённый документ может указывать на число или null. Код, принимающий внешние PDF, не должен использовать небезопасное приведение только на основании имени ключа.
Не находится ресурс страницы
Resources может наследоваться от родительского узла Pages. Нужно пройти Parent или использовать метод получения унаследованного значения. Внутри Form XObject действует собственный словарь ресурсов, поэтому поиск шрифта только на странице не охватывает вложенную форму. При разборе content stream оператор Do ведёт к XObject, который затем анализируется рекурсивно с ограничением глубины.
Копирование страницы теряет ссылку или поле
Проверяют, входит ли соответствующая аннотация, AcroForm и объект назначения в область копирования. Визуальное содержимое страницы и интерактивная структура связаны, но не идентичны. Если задача требует сохранить формы, нужно перенести корневой AcroForm, поля, widget-аннотации и обновить ссылки. Если интерактивность не нужна, безопаснее заранее определить политику сплющивания и протестировать печатный вид.
Повреждённый вход вызывает чрезмерное время работы
Для внешних документов устанавливают ограничения размера, числа объектов, глубины наследования и рекурсии XObject. Обработку выполняют с тайм-аутом и лимитом памяти на уровне процесса. Fuzzing harness и санитайзеры полезны для тестирования собственных обёрток. Даже при исправном парсере злоумышленник может создать документ с огромным декодированным потоком, поэтому лимиты прикладного уровня остаются необходимыми.
Контроль качества результата
Автоматический тест сначала проверяет код возврата, существование файла и минимальный размер. Затем PDFParser повторно открывает результат, сверяет число страниц, размеры и наличие ожидаемых ресурсов. Для генератора счёта можно извлечь текстовые фрагменты или хотя бы найти известные объекты. Такой round-trip обнаруживает ошибки структуры раньше, чем документ попадёт пользователю.
Визуальная проверка выполняется рендерингом страниц в PNG и сравнением с эталоном. Небольшие различия сглаживания допустимы, поэтому полезны пороги и маски динамических областей, например даты или номера. Для макета важны смещения, пропавшие глифы, обрезанные строки и изменения переноса. Эталон обновляют только после осознанного просмотра diff, иначе регрессия станет новой нормой.
Документ открывают минимум в двух независимых просмотрщиках, если он использует аннотации, прозрачность, шифрование или нестандартные шрифты. Один просмотрщик может автоматически исправить ошибку и скрыть проблему. Для печатного процесса дополнительно выполняют preflight: проверяют цветовые пространства, встраивание шрифтов, размер страницы, выход элементов за обрез и требования PDF/A или PDF/X, если они заявлены.
Безопасность входов тестируют отдельным набором повреждённых PDF, JPEG, PNG, TIFF и шрифтов. Сборка с AddressSanitizer и UndefinedBehaviorSanitizer помогает обнаружить выходы за границы и неопределённое поведение. Fuzzing направляют на парсер и декодеры, но найденный сбой закрепляют обычным регрессионным тестом с минимальным файлом. После обновления зависимостей набор прогоняют заново.
- Проверить число страниц и геометрию каждой ориентации.
- Скопировать кириллический текст из просмотрщика и сравнить с исходной строкой.
- Убедиться, что изображения не искажены и не потеряли прозрачность.
- Проверить внутренние ссылки, URI и области нажатия аннотаций.
- Открыть документ с правильным и неправильным паролем, если включено шифрование.
- Сравнить размер файла и число повторяющихся ресурсов с базовым уровнем.
- Пройти все страницы парсером, а не ограничиваться первой.
- Проверить печать или растровый рендеринг с целевым разрешением.
Организация кода вокруг PDFHummus
Поддерживаемый проект редко вызывает PDFWriter напрямую из бизнес-логики. Удобнее разделить данные, компоновку и PDF-адаптер. Модель документа содержит суммы, строки и изображения без координат. Layout рассчитывает страницы, прямоугольники и переносы. Адаптер преобразует готовый план в вызовы PDFHummus. Такое разделение позволяет тестировать расчёт таблицы без бинарного PDF и менять библиотеку записи, не переписывая правила предметной области.
Единицы измерения оформляют типами или функциями pt, mm и inch. Это предотвращает смешение миллиметров с пунктами. Прямоугольники описывают понятными полями left, bottom, width, height, а преобразование к PDFRectangle выполняют в одном месте. Для интерфейса с верхним левым началом координат создают функцию преобразования, принимающую высоту страницы. Тогда формулы не размножаются по шаблонам.
Ресурсы кэшируют в объекте сеанса документа. Ключ шрифта включает путь и индекс лица; ключ изображения — хэш байтов и параметры обработки; ключ формы — идентификатор шаблона. Кэш не должен переживать PDFWriter, потому что номера объектов принадлежат конкретному файлу. Для параллельной генерации каждый поток получает собственный сеанс, а неизменяемые исходные байты шрифтов и изображений можно разделять на более высоком уровне.
Ошибки адаптера переводят в доменные исключения или ожидаемые результаты. Вместо eFailure журнал сообщает: не удалось встроить логотип клиента, файл PNG повреждён или страница приложения 17 отсутствует. Пользовательское сообщение не обязано раскрывать внутренний путь, но технический лог должен содержать идентификатор задания и шаг. Так сбой можно воспроизвести без просмотра конфиденциального документа.
Производительность и параллельная обработка
Последовательная запись экономит память, потому что завершённые страницы можно сразу сериализовать. Однако производительность всего конвейера часто ограничивается декодированием изображений и шрифтов, а не операторами PDF. Профилирование разделяет время чтения, декодирования, компоновки и записи. Если одна и та же фотография обрабатывается для каждого экземпляра, кэш подготовленного ресурса даст больший эффект, чем микрооптимизация вызовов линий.
Независимые документы можно создавать параллельно отдельными экземплярами PDFWriter. Совместное использование одного экземпляра между потоками без явной гарантии потокобезопасности не допускается. Общий кэш исходных файлов защищают синхронизацией или делают неизменяемым. Число рабочих потоков выбирают по памяти: многопоточное декодирование TIFF может исчерпать её раньше, чем загрузить все ядра.
Для одного очень большого документа распараллелить саму последовательную запись сложнее, потому что номера объектов и выходной поток общие. Можно заранее параллельно подготовить данные, изображения и расчёт макета, а сериализацию оставить одному потоку. Другой подход — создать независимые фрагменты PDF и затем объединить, но он усложняет общие шрифты, ссылки и оглавление. Решение принимают после измерения, а не из предположения, что больше потоков всегда быстрее.
В сервере задают лимиты не только на входной размер, но и на ожидаемое число страниц, пикселей изображений и время генерации. Задача, превысившая лимит, завершается контролируемо и удаляет временный файл. Метрики включают длительность StartPDF–EndPDF, размер результата, число страниц и ошибки по типам ресурсов. Резкий рост средней величины файла помогает обнаружить потерю переиспользования ресурсов ещё до жалоб.
Метаданные, каталог и служебные словари
Документный каталог связывает дерево страниц с дополнительными структурами: метаданными, именами, outline, формами и настройками просмотра. Для простого генератора достаточно того, что создаёт PDFWriter, но прикладные требования часто включают заголовок, автора, тему и ключевые слова. Эти значения записывают согласованно и проверяют в свойствах документа. Не стоит помещать в метаданные внутренние пути, имя сервера или идентификатор базы, если файл уходит за пределы системы.
Словарь Info и поток XMP решают похожую задачу разными способами. Если приложение добавляет оба, значения должны совпадать, иначе разные инструменты покажут разные заголовки и даты. Дату PDF формируют с часовым поясом, а XML — в корректной кодировке. При модификации чужого документа заранее определяют политику: сохранить существующие поля, заменить только управляемые или удалить персональные данные. Случайное смешение старого автора и нового заголовка выглядит как ошибка происхождения документа.
PageMode и PageLayout влияют на первоначальное открытие: просмотрщик может показать миниатюры, закладки или разворот. Эти настройки не должны заменять структуру документа; например, включение панели закладок бесполезно без outline. ViewerPreferences могут запрашивать скрытие панелей или центрирование окна, но просмотрщик вправе игнорировать пожелание. Поэтому важная функция никогда не должна зависеть только от стартового режима интерфейса.
Идентификаторы файла в trailer используются шифрованием и некоторыми системами отслеживания. При полном создании их формируют один раз, при корректной инкрементальной модификации правила обновления отличаются. Не следует копировать идентификатор из шаблона во все выпущенные документы без анализа. Для детерминированных тестов динамические поля даты и ID можно нормализовать перед бинарным сравнением, но в рабочем файле они должны соответствовать выбранной политике.
Низкоуровневая запись объектов
ObjectsContext открывает доступ к созданию косвенных объектов, словарей, массивов и потоков. Обычно этот уровень нужен для функции, которой нет в готовом методе: собственного типа аннотации, дополнительного словаря каталога, ExtGState или специализированных метаданных. Работа начинается с получения нового ObjectID, записи заголовка объекта и последовательного формирования значения. После завершения контекста номер можно поместить в другие словари как indirect reference.
Поток состоит из словаря и байтов. Ключ Length должен соответствовать фактической длине закодированных данных; безопаснее пользоваться предоставленным механизмом stream object, чем вычислять длину вручную. Если применён Filter, декодирующие параметры должны согласовываться с байтами. Ошибка длины иногда не мешает одному просмотрщику, но ломает парсер, который строго следует xref и границам потока.
Имена PDF начинаются с косой черты и используют собственное экранирование. Строки бывают literal и hexadecimal; выбор влияет на представление, но не отменяет правил кодирования текста. Числа записывают с достаточной точностью, избегая NaN и бесконечностей. Для координат чрезмерное число знаков увеличивает файл без визуальной пользы, поэтому layout может округлять значения до разумной точности до передачи в content stream.
Расширяя каталог или страницу, нельзя перезаписать существующий ключ без понимания его назначения. Для массива Annots обычно добавляют элемент, сохраняя прежние; для Resources объединяют категории Font, XObject и ExtGState, а не заменяют весь словарь. При модификации косвенный словарь может быть общим для нескольких страниц, поэтому изменение на месте повлияет на все ссылки. В сомнительном случае создают новый словарь для конкретной страницы и копируют необходимые записи.
Алгоритм таблиц и разрывов страниц
Таблица начинается не с линий, а с измерения содержимого. Для каждой ячейки код определяет доступную ширину, разбивает текст на строки и вычисляет высоту по максимальному числу строк в ряду. К этому добавляются внутренние отступы и толщина границ. Только после расчёта всего ряда принимается решение, помещается ли он в оставшуюся область страницы. Такой порядок предотвращает ситуацию, когда текст уже нарисован, а нижняя граница ушла за пределы листа.
Если ряд не помещается, текущую страницу завершают, создают новую, повторяют шапку и рисуют ряд целиком. Очень высокий ряд, превышающий полезную высоту страницы, требует отдельной политики: разрешить разделение текста, уменьшить шрифт в допустимых пределах или вывести продолжение в нескольких рядах. Бесконечный цикл возникает, если алгоритм каждый раз переносит тот же ряд, но никогда не допускает его деление; защитой служит проверка высоты относительно полной области.
Границы лучше рисовать после текста или единым проходом для всей рассчитанной сетки. Если соседние ячейки каждая рисуют общий край, линия становится визуально толще. Координаты линий округляют одинаково, иначе при растеризации появляются микрозазоры. Заливку ячеек выводят первой, затем текст, затем границы, чтобы фон не перекрыл содержимое. Все операции ряда помещают между q и Q.
Колонтитулы резервируют в полезной области ещё до компоновки. Номер страницы неизвестен до формирования документа, если используется формат страница X из Y. Один вариант — сначала рассчитать весь layout без записи, второй — оставить фиксированное место и после определения Y наложить номера при модификации. Для потоковой генерации проще использовать только текущий номер либо заранее знать число страниц из данных.
Сложные таблицы проверяют наборами с длинными словами без пробелов, пустыми значениями, отрицательными числами, многострочной кириллицей и очень большим количеством рядов. Для неразрывной строки задают политику: обрезка с многоточием, уменьшение шрифта, перенос по символам или выход за рамку запрещён с ошибкой. Молчаливое переполнение даёт документ, который формально открылся, но содержит наложенный текст.
Шрифтовой конвейер в рабочем проекте
Файлы шрифтов поставляют вместе с приложением либо получают из гарантированного системного расположения. Полагаться на случайный шрифт рабочей станции опасно: на сервере его может не быть, а другая сборка файла даст иные метрики. Путь разрешают при запуске и проверяют тестовой строкой. Лицензию шрифта также учитывают: техническая возможность встраивания не означает разрешение распространять его внутри PDF.
Подмножество шрифта уменьшает размер, оставляя только использованные глифы. Это эффективно для счетов, но затрудняет последующее добавление новых символов в уже созданный ресурс. При инкрементальной модификации проще создать новый шрифтовой ресурс для добавленного текста. Если документ должен заполняться дальше другой системой, требования к полному встраиванию определяют заранее и проверяют preflight-инструментом.
Синтетический bold или italic не следует считать заменой настоящего начертания. Наклон матрицей меняет форму, а увеличение толщины рендерингом контура может ухудшить мелкий текст. Для семейства хранят отдельные файлы regular, bold, italic и bold italic. Layout выбирает начертание до измерения, потому что ширина строки меняется. Иначе заголовок, измеренный обычным шрифтом, после переключения на bold выйдет за колонку.
Для смешанных языков строку разбивают по доступности глифов. Сначала основной шрифт покрывает кириллицу и латиницу, затем отдельный fallback — символы CJK или специальные пиктограммы. Фрагменты измеряют и выводят последовательно, сохраняя общий baseline. Эмодзи часто требуют цветных таблиц шрифта, которые не следует считать автоматически поддержанными; надёжнее использовать подготовленное изображение или проверенный монохромный глиф.
При изменении размера шрифта интерлиньяж и вертикальные отступы пересчитывают, а не масштабируют только оператор Tf. Для верхних и нижних индексов используют отдельный размер и смещение baseline. Подчёркивание рисуют линией по метрикам, потому что обычный текстовый оператор не добавляет его автоматически. Эти детали лучше инкапсулировать в компонент RichText, чтобы шаблоны не повторяли математику.
Воспроизводимость и диагностика бинарных различий
Два визуально одинаковых PDF могут отличаться байт в байт из-за дат, идентификаторов, порядка объектов, сжатия и подмножеств шрифтов. Поэтому бинарный хэш подходит для контроля неизменного fixture только при детерминированной конфигурации. Для функциональных тестов надёжнее сравнивать число страниц, извлечённый текст, геометрию и растровый результат. Если нужен цифровой артефакт с повторяемым хэшем, все динамические поля и порядок входных данных фиксируют.
При регрессии сначала сравнивают структуру: какие объекты добавились, изменились ли Resources и размеры потоков. Затем декодируют content stream и сопоставляют операторы. Это быстрее, чем рассматривать весь бинарный diff со сжатыми байтами. Для изображений вычисляют хэш исходных данных и параметры матрицы; для шрифтов — имя ресурса, список глифов и наличие ToUnicode.
Лог задания содержит конфигурацию сборки, включённые обработчики форматов, архитектуру, идентификатор шаблона и хэши входов. Сам документ в журнал не копируют. При жалобе эти данные позволяют восстановить среду и определить, отличается ли вход. Если результат генерируется в контейнере, фиксируют образ и пакет зависимостей, чтобы обновление системной LibTiff или OpenSSL не произошло незаметно.
Временные файлы создают в каталоге с достаточным свободным местом и уникальным именем. После успешной проверки выполняют атомарное перемещение в назначение. При сбое удаляют незавершённый результат, но журнал сохраняют по политике диагностики. Периодическая уборка защищает сервер от накопления файлов после принудительно завершённых процессов.
Развёртывание в приложении и сервисе
При статической линковке проще доставить один бинарный файл, но размер увеличивается, а обновление уязвимой зависимости требует пересборки приложения. Динамическая линковка уменьшает дублирование и позволяет обновлять системные библиотеки, однако требует точного контроля ABI и путей загрузки. Выбранный способ тестируют на чистой машине, а не только в среде разработчика, где нужная DLL или so уже установлена случайно.
В контейнере build-stage содержит компилятор и CMake, а runtime-stage — только исполняемый файл, необходимые динамические библиотеки, шрифты и шаблоны. После копирования выполняют smoke test, который генерирует PDF, повторно открывает его и проверяет страницу. Это выявляет отсутствующий шрифт, OpenSSL или библиотеку изображений ещё при сборке образа. Права каталога временных файлов проверяют отдельным непривилегированным пользователем.
Сервисный API принимает не произвольные пути к файлам, а идентификаторы загруженных ресурсов. Это предотвращает чтение системных файлов через шаблон. Имена выходов очищают, а размер и тип каждого входа проверяют до парсера. Обработка выполняется с лимитом времени, памяти и диска. Отмена запроса должна закрывать поток и удалять временный PDF, иначе нагрузочный тест быстро заполнит хранилище.
Обновление PDFHummus и зависимостей проводят через тестовый контур. Сначала собирают библиотеку с теми же флагами, запускают unit, corpus и визуальные тесты, затем сравнивают производительность и размеры результатов. Особое внимание уделяют парсеру и декодерам, потому что исправления безопасности могут сделать ранее принимавшийся повреждённый файл ошибочным. Такое изменение лучше вернуть клиенту как понятную ошибку входа, а не как внутренний сбой.
Когда PDFHummus подходит лучше всего
PDFHummus рационален для C++-систем, которым нужен детерминированный выпуск PDF и прямой контроль над страницами, потоками и объектами. Он хорошо проявляет себя, когда макет можно выразить координатами, документы генерируются массово, а слияние и наложение должны выполняться внутри существующего процесса без запуска тяжёлого внешнего движка. Возможность сочетать высокоуровневую запись с PDFParser и copying context уменьшает число отдельных инструментов в конвейере.
Выбор требует готовности реализовать собственную компоновку и внимательно работать со спецификацией. Для единичной ручной правки визуальный редактор быстрее. Для HTML-шаблонов с развитым CSS удобнее движок веб-разметки. Для структурной нормализации без рисования может быть проще QPDF. Но когда нужны C++, потоковая генерация, повторно используемая графика, изображения, шрифты и точечное дополнение существующих страниц, набор PDFHummus закрывает эти задачи в одном API.
Надёжный результат получается не из одного удачного примера, а из дисциплины: фиксированной сборки, проверенных шрифтов, баланса контекстов, обработки EStatusCode, тестов на повреждённых входах и повторного открытия готового файла парсером. Если к этому добавить визуальный regression test и контроль дубликатов ресурсов, генератор остаётся предсказуемым при росте объёма, новых языках и более сложных шаблонах. Именно такой процесс превращает низкоуровневый контроль PDF в практическое преимущество, а не в источник случайных ошибок.