pdf-lib позволяет создавать PDF с нуля, дополнять готовые документы текстом, изображениями и векторной графикой, переносить и объединять страницы, заполнять AcroForm-поля, вкладывать файлы и управлять метаданными. Работа строится вокруг объектов PDFDocument, PDFPage и PDFForm: документ загружается из массива байтов, нужные страницы и поля изменяются методами API, а результат сохраняется в Uint8Array для файла, ответа сервера или предпросмотра.
Основной рабочий процесс состоит из четырёх действий: получить исходные байты, вызвать PDFDocument.load() или PDFDocument.create(), выполнить операции над страницами и ресурсами, затем дождаться pdfDoc.save(). Такой порядок одинаков для заполнения анкеты, наложения штампа, сборки отчёта и формирования комплекта из нескольких документов. В коде нет скрытого состояния редактора: каждое изменение явно связано с объектом документа, поэтому последовательность легко покрыть тестами и повторить для сотен файлов.
Управление выполняется через JavaScript или TypeScript, поэтому вместо панелей и меню используются методы с параметрами координат, шрифта, цвета, прозрачности и размеров. Это даёт точный контроль над результатом, но требует самостоятельно построить экран выбора файла, обработку ошибок, индикатор выполнения и выдачу сохранённого PDF. На практике удобнее сначала отладить преобразование на одном небольшом образце, а затем подключать его к форме сайта, серверному маршруту или мобильному сценарию.
Скачать pdf-lib
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет визуального интерфейса
- Нет HTML/CSS-рендера
- Не читает обычный текст
Как устроен рабочий процесс в pdf-lib
PDFDocument представляет весь файл: каталог объектов, дерево страниц, ресурсы, формы, вложения и свойства документа. PDFPage отвечает за геометрию конкретного листа и команды рисования. PDFForm предоставляет типизированный доступ к интерактивным полям. Вспомогательные сущности PDFFont, PDFImage и PDFEmbeddedPage создаются через документ, а затем используются на одной или нескольких страницах. Такой порядок важен: объект, внедрённый в один PDFDocument, нельзя без копирования считать ресурсом другого документа.
При создании нового файла используется await PDFDocument.create(). Метод addPage() без аргументов добавляет лист стандартного размера, а массив из двух чисел задаёт ширину и высоту в пунктах. Для существующего файла PDFDocument.load() принимает Uint8Array, ArrayBuffer, строку Base64 или data URI. После загрузки getPageCount() возвращает число страниц, getPages() — массив объектов PDFPage, а getPage(index) даёт одну страницу по индексу, начиная с нуля.
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
const page = pdfDoc.addPage([595.28, 841.89])
page.drawText('Сформированный документ', {
x: 48,
y: 790,
size: 18,
font,
color: rgb(0.1, 0.1, 0.1),
})
const bytes = await pdfDoc.save()
Результат save() — не путь к файлу и не поток, а байтовый массив. В серверном коде его передают в файловую систему или тело HTTP-ответа. В браузерном сценарии из массива создают Blob с MIME-типом application/pdf и объектный адрес, который можно открыть во встроенном просмотрщике либо передать функции сохранения. Метод saveAsBase64() полезен, когда принимающая система работает со строками, но Base64 увеличивает объём и создаёт дополнительную копию данных в памяти.
Документ изменяется в памяти до вызова save(). Если операция состоит из нескольких стадий — например, заполнение полей, добавление штампа и удаление лишних страниц, — сохранять промежуточный файл после каждой стадии обычно не нужно. Один вызов сериализации уменьшает число копирований и упрощает обработку ошибки. Промежуточное сохранение оправдано только как диагностическая точка, когда надо выяснить, на какой операции сторонний просмотрщик перестаёт открывать результат.
Координаты, размеры страниц и поля обрезки
Координатная система PDF отличается от привычной вёрстки веб-страницы: начало обычно находится в левом нижнем углу, x растёт вправо, y — вверх. Поэтому заголовок возле верхнего края размещают не фиксированным y=40, а выражением height - верхний отступ - размер шрифта. Метод page.getSize() возвращает текущие width и height, а getWidth() и getHeight() удобны в коротких формулах. Размеры задаются в пунктах; 72 пункта соответствуют одному дюйму.
У страницы несколько рамок. MediaBox задаёт физическую область листа, CropBox — видимую область, BleedBox — припуск для печати, TrimBox — итоговый формат после обрезки, ArtBox — границы значимого содержимого. Для простого отчёта обычно достаточно getSize(), но при штамповке чужих макетов полезно проверить CropBox и его смещение. Иначе подпись, рассчитанная от нуля, может попасть за видимую границу или оказаться сдвинутой относительно содержимого.

Методы setSize(), setWidth() и setHeight() меняют размеры листа, но не масштабируют уже нарисованное содержимое. Если требуется вписать исходную страницу в новый формат, безопаснее внедрить её как PDFEmbeddedPage и нарисовать на новом листе с нужными width и height. Простое изменение MediaBox может обрезать объекты или оставить их в прежних координатах. Для печатного задания отдельно проверьте ориентацию, поворот страницы и наличие нестандартного начала координат.
У страниц с установленным Rotate визуальный верх не всегда совпадает с геометрическим верхом исходной системы координат. При наложении штампа на смешанный комплект сначала прочитайте rotation через getRotation(), затем либо вычислите координаты для каждого угла, либо создайте новую страницу и поместите исходную как внедрённую. Такой подход длиннее, но предсказуемее для документов, где часть листов альбомная, часть книжная, а сканер записал поворот в словарь страницы вместо реального разворота содержимого.
Текст: шрифты, строки и точные измерения
Для текста используется page.drawText(). В параметрах указывают x, y, size, font, color, rotate, opacity и lineHeight. Многострочная строка разделяется символами перевода строки; lineHeight задаёт расстояние между базовыми линиями. Если отдельные вызовы должны наследовать одинаковые настройки, можно предварительно вызвать page.setFont(), setFontSize(), setFontColor() и setLineHeight(), а в drawText передавать только отличающиеся значения.
Стандартные четырнадцать PDF-шрифтов удобны для латинского текста и небольших служебных меток. Они не требуют передачи файла шрифта, но используют ограниченную кодировку. Кириллица, греческие символы, многие знаки валют и иероглифы могут вызвать сообщение вида WinAnsi cannot encode. Решение — зарегистрировать fontkit, получить байты TTF или OTF, внедрить шрифт и передать полученный PDFFont в drawText либо обновление внешнего вида полей.
import { PDFDocument, rgb } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
const fontBytes = await readFontBytes()
const font = await pdfDoc.embedFont(fontBytes, { subset: true })
const page = pdfDoc.addPage()
page.drawText('Счёт № 184: оплачено', {
x: 50,
y: 760,
size: 16,
font,
color: rgb(0, 0, 0),
})
Параметр subset при внедрении пользовательского шрифта позволяет включить только использованные глифы и уменьшить итоговый файл. Он особенно полезен для больших гарнитур с кириллицей и азиатскими наборами. Однако каждый отдельный внедрённый экземпляр создаёт собственный ресурс. Если один шрифт нужен на десятках страниц, внедрите его один раз и повторно используйте тот же объект PDFFont, а не вызывайте embedFont() внутри цикла.
PDFFont.widthOfTextAtSize() вычисляет ширину строки при заданном кегле, а heightAtSize() помогает построить подложку, рамку или вертикальное выравнивание. Для таблиц лучше заранее определить ширины столбцов и функцию переноса слов. drawText() не является полноценным движком макета: он не знает о CSS, не переносит сложные блоки как браузер и не разрывает таблицу автоматически. Разработчик сам следит за текущей координатой y, высотой строки и переходом на новый лист.
Простой перенос строится так: строка разбивается на слова, к текущей строке пробуется добавить следующее слово, затем измеряется widthOfTextAtSize(). Если ширина превышает доступную область, накопленная строка рисуется, y уменьшается на lineHeight, а слово становится началом следующей строки. Перед рисованием проверяют нижний предел; при его достижении добавляют новую страницу, повторяют шапку и продолжают. Для длинных отчётов такая небольшая функция даёт более стабильный результат, чем ручная расстановка координат для каждого абзаца.
Поворот задаётся объектом degrees(), radians() или rotations, поддерживаемым API. При диагональном водяном знаке координаты относятся к точке начала текста, а не к центру его визуальной рамки. Поэтому сначала измерьте строку, вычислите центр листа и скорректируйте x и y. Прозрачность помогает сохранить читаемость исходного содержимого, но слишком светлая надпись может исчезнуть при чёрно-белой печати. Для юридически значимого штампа лучше проверить результат в печати, а не только на экране.

Изображения JPEG и PNG
JPEG внедряется через pdfDoc.embedJpg(), PNG — через embedPng(). Оба метода принимают байты, Base64 или data URI и возвращают PDFImage. Свойства width и height отражают исходные размеры ресурса, а scale(factor) рассчитывает пропорциональные размеры для рисования. Метод scaleToFit(width, height) удобен для размещения фотографии в заданном прямоугольнике без искажения пропорций.
page.drawImage() принимает x, y, width, height, rotate, xSkew, ySkew, opacity и blendMode. Если передать произвольные width и height, картинка растянется. Для логотипа обычно берут размеры из scale(), для фотографии — scaleToFit(), а для фона сознательно заполняют весь лист. PNG сохраняет альфа-канал, поэтому подходит для печатей и значков с прозрачностью. JPEG меньше по размеру для фотографий, но не поддерживает прозрачный фон.
Один и тот же PDFImage можно рисовать на нескольких страницах. Это экономнее, чем повторно внедрять идентичные байты. При пакетном формировании сертификатов логотип внедряют до цикла, затем на каждой странице меняют только координаты и размеры. Если изображения приходят от пользователей, до embedJpg() или embedPng() следует проверить реальный формат, размер и разумный предел пикселей: расширение имени не гарантирует, что внутри действительно JPEG или PNG.
Библиотека не выполняет универсальное декодирование TIFF, WebP, HEIC и офисных форматов. Такие файлы надо заранее преобразовать в JPEG или PNG. Для сканов полезно также изменить разрешение и степень сжатия до внедрения: помещение фотографии на страницу с уменьшенными width и height не уменьшает исходное число пикселей, поэтому большой снимок останется тяжёлым ресурсом. Оптимизацию лучше делать на входе, сохраняя достаточное качество для предполагаемой печати.

Векторная графика и элементы оформления
Для простых схем и форм не обязательно готовить картинку. PDFPage предоставляет drawLine(), drawRectangle(), drawSquare(), drawCircle(), drawEllipse() и drawSvgPath(). Цвет задаётся через rgb(), cmyk() или grayscale(). У заливки и границы можно отдельно настроить прозрачность; у линий — толщину, начальную и конечную точки, а также стиль окончания. Эти примитивы подходят для таблиц, разделителей, рамок, диаграмм, меток обрезки и фоновых плашек.
drawSvgPath() принимает строку команд пути SVG, но не весь SVG-документ. Он удобен для иконки или контура, когда путь уже известен. Стили CSS, текстовые узлы, фильтры и внешние ресурсы из SVG автоматически не переносятся. Сложный значок стоит предварительно свести к одному или нескольким path и явно указать цвет, масштаб и координаты. При масштабировании обратите внимание, что путь может иметь собственное начало координат и отрицательные значения.
Графические операции добавляются в поток содержимого страницы в порядке вызовов. Поздняя команда обычно рисуется поверх предыдущей. Поэтому подложку под текст нужно создавать раньше текста, а маскирующий прямоугольник — после исходного содержимого. Однако белый прямоугольник не удаляет скрытые данные: старый текст остаётся в PDF и может быть извлечён другим инструментом. Для настоящего редактирования конфиденциальных сведений требуется корректное безвозвратное удаление содержимого и очистка объектов, которой высокоуровневый API pdf-lib не предоставляет.
Прозрачность и режим смешивания позволяют сделать мягкий водяной знак, но разные просмотрщики и печатные цепочки могут отображать сложные сочетания немного по-разному. Для корпоративного шаблона лучше ограничиться обычной альфа-прозрачностью, стандартным RGB или CMYK и простыми геометрическими объектами. После генерации откройте тестовый файл в нескольких программах и распечатайте контрольную страницу, если документ будет уходить в типографию.

Изменение существующего PDF без разрушения исходной разметки
PDFDocument.load() читает существующий файл и позволяет добавить новые команды к страницам, изменить дерево страниц, свойства и поля. Наиболее надёжный сценарий — наложение: исходный поток содержимого остаётся, а новый текст, изображение или графика добавляются сверху. Так создают номер заказа, штамп проверки, подпись, QR-код, дату или заметную диагональную маркировку. Координаты определяются по шаблону либо вычисляются из размеров страницы.
Редактирование фразы, уже нарисованной в обычном потоке содержимого, устроено иначе. pdf-lib не извлекает обычный текст страницы в виде строк и не предлагает команду “найти и заменить”. PDF может хранить буквы отдельными глифами, в произвольном порядке, с матрицами преобразования и встроенными шрифтами. Поэтому библиотека умеет менять текстовые поля формы, но не превращает произвольную страницу в редактируемый текстовый документ.
Визуальную замену иногда имитируют: область закрывают прямоугольником цвета фона и поверх рисуют новую строку. При однородном белом фоне это выглядит приемлемо, но исходные символы остаются внутри файла, а фон может содержать изображение, сетку или полупрозрачные элементы. Такой приём нельзя использовать как удаление персональных данных. Он годится лишь для некритичной визуальной поправки, когда сохранение скрытого содержимого допустимо.
Если документ представляет собой скан, на странице может вообще не быть текста — только изображение. pdf-lib способна наложить новые объекты, но не распознаёт скан. Для поиска, извлечения и замены требуется отдельный OCR-процесс. Результат OCR можно использовать для вычисления координат, а затем рисовать отметки через pdf-lib, но распознавание и построение текстового слоя остаются задачей другого компонента.
Страницы: добавление, удаление, перестановка и копирование
addPage() добавляет лист в конец, insertPage(index, page) вставляет в указанную позицию, removePage(index) удаляет одну страницу. Для перестановки нескольких листов удобно создать новый PDFDocument и копировать страницы в нужном порядке. Индексы начинаются с нуля, поэтому пользовательский номер страницы 1 соответствует индексу 0. Перед операцией проверяйте диапазон, иначе ошибка в данных может остановить весь пакет.
copyPages(sourceDoc, indices) копирует выбранные страницы между экземплярами PDFDocument и возвращает массив новых PDFPage. После этого каждую страницу добавляют или вставляют в целевой документ. Метод подходит для объединения, разделения и выборочной сборки. Порядок индексов определяет порядок возвращаемых страниц; повтор одного индекса позволяет поместить одну исходную страницу несколько раз.
const target = await PDFDocument.create()
const sourceA = await PDFDocument.load(bytesA)
const sourceB = await PDFDocument.load(bytesB)
const pagesA = await target.copyPages(sourceA, [0, 2, 4])
for (const page of pagesA) target.addPage(page)
const [coverB] = await target.copyPages(sourceB, [0])
target.insertPage(0, coverB)
const mergedBytes = await target.save()
Копирование переносит ресурсы страницы, но не следует считать его эквивалентом клонирования всей логики документа. Интерактивные формы, сценарии, закладки, сложные связи и глобальные структуры могут требовать отдельной проверки. Если задача — объединить анкеты и сохранить поля доступными, протестируйте реальные шаблоны: одинаковые имена полей в разных файлах и общая AcroForm-структура способны давать неожиданный результат. Для выдачи неизменяемого комплекта часто безопаснее сначала заполнить и сплющить каждую форму, а затем объединять страницы.
При разделении большого файла не загружайте его заново для каждой страницы. Один раз получите sourceDoc, затем для каждого выходного документа вызовите copyPages() с нужным диапазоном и сохраните результат. Если выходов много, следите за памятью и обрабатывайте их последовательно. Десятки одновременных копий большого PDF удерживают исходные и новые объекты одновременно, что особенно заметно в ограниченном серверном процессе или мобильной среде.

Внедрение страницы как графического ресурса
copyPages() добавляет самостоятельную страницу в дерево целевого документа. embedPage() и embedPdf() решают другую задачу: превращают страницу исходного PDF в ресурс, который можно нарисовать внутри новой страницы через drawPage(). Это полезно для миниатюр, двух страниц на одном листе, бланка на фоне, фрагмента схемы или масштабирования нестандартного формата.
У PDFEmbeddedPage есть исходные width и height, методы scale() и scaleToFit(). В drawPage() можно передать width и height либо xScale и yScale; если указаны оба варианта, явные размеры имеют приоритет. Параметры rotate и opacity работают аналогично изображению. При создании листа “две страницы на одной” заранее вычислите поля, промежуток и масштаб для каждой половины, затем поместите страницы в разные прямоугольники.
embedPage() допускает ограничивающую рамку, поэтому можно взять только область исходного листа. Координаты рамки задаются в системе исходной страницы и требуют внимательной проверки CropBox и поворота. Неправильные left, bottom, right и top дают пустой или обрезанный результат. Надёжная отладка — сначала внедрить всю страницу, убедиться в ориентации, затем постепенно сужать рамку и рисовать её границы на тестовом листе.

Внедрённая страница становится частью содержимого нового листа, а не интерактивной страницей исходного документа. Поля формы, ссылки и аннотации в таком представлении не должны рассматриваться как сохранённые элементы интерфейса. Если интерактивность важна, используйте копирование страницы и отдельно проверяйте структуру документа. Если нужен только внешний вид — внедрение даёт удобный способ масштабировать и повторно использовать готовый макет.
Формы AcroForm: поиск полей и проверка имён
PDFDocument.getForm() возвращает PDFForm. Метод getFields() перечисляет все поля, а каждый объект сообщает имя через getName(). Это первый диагностический шаг перед заполнением чужого шаблона. Визуальная подпись возле поля не обязана совпадать с техническим именем, а вложенные имена могут содержать точки и пробелы. Не стоит угадывать их по надписи на странице: выведите список имён и типов в журнал или служебный экран.
Для типизированного доступа используются getTextField(), getCheckBox(), getRadioGroup(), getDropdown(), getOptionList() и getButton(). Если поле существует, но имеет другой тип, будет выброшена ошибка. Поэтому в универсальном обработчике полезно пройти getFields(), проверить constructor.name и сопоставить данные по заранее утверждённой схеме. Ошибка типа должна быть замечена на тесте шаблона, а не после получения сотен заполненных файлов.
Текстовое поле заполняется setText(), флажок — check() или uncheck(), радиогруппа и раскрывающийся список — select(), список — select() с одним или несколькими значениями в зависимости от настроек. Для кнопочного поля можно установить изображение через setImage(). getText(), isChecked(), getSelected() и getOptions() помогают проверить исходное состояние и допустимые значения до изменения.

У текстового поля есть параметры максимальной длины, многострочности, выравнивания, comb-разметки и размера шрифта. setMaxLength() ограничивает число символов; enableMultiline() разрешает несколько строк; setAlignment() меняет выравнивание. Combing делит область на одинаковые ячейки, но требует максимальной длины и несовместим с некоторыми режимами. Перед изменением этих свойств сохраните контрольный файл и проверьте его в нескольких просмотрщиках.
Богатый текст внутри полей не является сильной стороной API. При setText() богатое поле переводится к обычному тексту, а сложная разметка не сохраняется как редактируемая RTF-структура. Если шаблон зависит от цветных фрагментов, разных шрифтов внутри одного поля или сценариев Acrobat, лучше заранее упростить форму либо рисовать окончательное содержимое на странице и сплющить результат.
Внешний вид полей, кириллица и сплющивание
Значение поля и его видимое представление хранятся в разных частях PDF. После setText() поле помечается как изменённое, а при save() библиотека обновляет appearance stream. По умолчанию для текста используется Helvetica с WinAnsi-кодировкой. Поэтому русская строка может вызвать ошибку даже тогда, когда сам шаблон визуально использует другой шрифт. Нужный шрифт следует внедрить и передать в form.updateFieldAppearances(font).
const form = pdfDoc.getForm()
const customer = form.getTextField('order.customer')
const total = form.getTextField('order.total')
customer.setText('ООО Пример')
total.setText('125 400 ₽')
pdfDoc.registerFontkit(fontkit)
const font = await pdfDoc.embedFont(fontBytes, { subset: true })
form.updateFieldAppearances(font)
const result = await pdfDoc.save()
Если обновить значения, но не сформировать корректный appearance stream, одни просмотрщики покажут текст, а другие оставят поле пустым до фокусировки. Явный вызов updateFieldAppearances() с подходящим шрифтом делает результат предсказуемее. При нестандартном дизайне можно обновлять внешность отдельного поля и передавать собственный provider, однако это уже требует понимания виджетов, рамок и низкоуровневых операторов рисования.
form.flatten() переносит текущий внешний вид виджетов в поток содержимого страниц и удаляет поля с относящимися к ним аннотациями. После этого значения выглядят как обычная часть страницы и больше не редактируются через PDFForm. Сплющивание удобно перед объединением, отправкой на печать и передачей в систему, которая плохо отображает интерактивные формы. Оно должно выполняться после заполнения и обновления внешнего вида, иначе в страницу попадёт устаревшее или пустое представление.

Перед flatten() сохраните возможность повторно сформировать документ из исходного шаблона и данных. Сплющенный файл не является удобной основой для последующего исправления. Практичная архитектура хранит шаблон отдельно, значения — в базе или JSON, а PDF считает производным результатом. При изменении заказа система снова загружает чистый шаблон, применяет актуальные данные и создаёт новый файл.
XFA-формы отличаются от AcroForm. Если документ содержит XFA, библиотека может удалить XFA-часть через deleteXFA(), но это помогает только тогда, когда внутри также присутствуют usable AcroForm-поля. Чистую динамическую XFA-форму нельзя автоматически превратить в обычную анкету. Перед внедрением процесса проверьте конкретный бланк и убедитесь, что getFields() действительно возвращает ожидаемые поля.
Создание собственных полей формы
PDFForm умеет не только заполнять существующие поля, но и создавать новые: createTextField(), createCheckBox(), createRadioGroup(), createDropdown(), createOptionList() и createButton(). После создания поле добавляют на страницу методом addToPage() с координатами, размерами, цветом, толщиной рамки и шрифтом. Для радиогруппы отдельные варианты добавляются как виджеты с собственными значениями.
Имена полей должны быть уникальными и стабильными. Хорошая схема использует иерархические имена вроде customer.name, customer.phone, delivery.method. Это упрощает сопоставление данных и вывод списка. Изменение имени поля в уже выпущенном шаблоне ломает код, поэтому имена лучше зафиксировать вместе с контрактом данных. Визуальную подпись рисуйте отдельно через drawText(), не полагаясь на то, что имя поля будет показано пользователю.
При построении формы задайте достаточную высоту и размер шрифта, проверьте табуляцию и печать. Удобство заполнения зависит не только от наличия поля: маленькая область, слабый контраст рамки или отсутствие места для длинного значения делают бланк непрактичным. Для обязательных полей можно установить required, для нередактируемых — readOnly. Эти флаги помогают совместимым просмотрщикам, но сервер всё равно должен валидировать полученные данные самостоятельно.
PDF-кнопка в AcroForm не равна HTML-кнопке и не гарантирует выполнение произвольной логики во всех просмотрщиках. pdf-lib позволяет создать кнопку и настроить внешний вид, однако сценарии JavaScript внутри PDF поддерживаются неодинаково и часто блокируются политиками безопасности. Для надёжного процесса действие лучше выполнять в веб-приложении, а PDF использовать как форму данных или конечный документ.
Метаданные и параметры открытия
PDFDocument предоставляет методы setTitle(), setAuthor(), setSubject(), setKeywords(), setCreator(), setProducer(), setCreationDate() и setModificationDate(). Соответствующие get-методы читают значения. Метаданные полезны для поиска, документооборота и диагностики, но не заменяют видимый заголовок на странице. Если свойства должны быть воспроизводимыми в тестах, задавайте даты явно; автоматическое обновление времени делает байты разных запусков отличающимися.
При load() можно передать updateMetadata: false, чтобы чтение документа не меняло служебные свойства автоматически. Это важно, когда требуется минимально вмешаться в чужой файл или сравнить результат на уровне объектов. Если процесс сознательно создаёт новый производный документ, напротив, стоит установить creator, producer и modification date в соответствии с правилами организации.
ViewerPreferences управляют тем, как совместимый просмотрщик открывает файл: отображение панели, режим страницы, направление чтения и некоторые параметры печати. Эти настройки являются пожеланиями, а не обязательной командой. Браузерный просмотрщик, настольная программа и мобильное приложение могут интерпретировать их по-разному. Поэтому критически важную инструкцию пользователю следует размещать на самой странице, а не только в настройках открытия.
Метод attach() добавляет вложение и принимает байты, имя, MIME-тип, описание и даты. Так можно приложить исходный CSV, изображение, XML или другой PDF. Вложения поддерживаются не всеми просмотрщиками одинаково: некоторые браузеры не показывают их панель, хотя данные присутствуют. Если получатель обязан получить дополнительный файл, не полагайтесь только на вложение — выдайте его отдельно или используйте проверенную систему документооборота.
Что pdf-lib не делает и как построить соседние этапы
Библиотека не извлекает обычный текст страницы. Она может прочитать значение PDFTextField, но не возвращает готовые абзацы, слова и координаты произвольного содержимого. Для индексации, поиска и анализа макета нужен парсер текста или движок визуального распознавания. После получения координат pdf-lib можно использовать для подсветки, нумерации или создания производного файла, но сам этап извлечения выполняется отдельно.
Она также не удаляет и не заменяет обычные текстовые операторы на странице через высокоуровневый API. Добавление новой надписи не означает редактирование старой. Для коррекции исходного договора лучше изменить исходный документ и заново экспортировать PDF. Для автоматического заполнения используйте AcroForm-поля. Для визуального штампа, который не должен изменять старый текст, наложение подходит хорошо.
HTML и CSS не являются входным языком макета. Нельзя передать фрагмент страницы с flexbox, таблицей стилей и веб-шрифтом и ожидать браузерный рендер. Если дизайн уже существует как HTML, логичнее сформировать PDF движком браузера, а pdf-lib применить после этого: объединить страницы, вложить файл, заполнить поля, добавить номера или метаданные. Такое разделение использует сильную сторону каждого инструмента.
Шифрование не поддерживается как штатный сценарий. При загрузке зашифрованного файла выбрасывается EncryptedPDFError. Параметр ignoreEncryption позволяет обойти раннюю проверку, но не расшифровывает содержимое и не обещает корректного изменения. Надёжный процесс сначала получает незашифрованный PDF законным способом, выполняет операции, а затем при необходимости применяет шифрование специализированным компонентом.
OCR, цифровая подпись, полноценное удаление скрытого содержимого, конвертация офисных файлов и визуальное сравнение документов не входят в высокоуровневые возможности. Их подключают отдельными этапами. Важно не маскировать отсутствие функции внешне похожим действием: нарисованная картинка подписи не является криптографической подписью, белая плашка не является удалением данных, а изображение текста не становится доступным для поиска.
Работа в Node.js
На сервере входные байты обычно поступают из fs, загрузки файла, объектного хранилища или ответа другого сервиса. Buffer совместим с Uint8Array, поэтому его можно передать в PDFDocument.load(). После save() полученный Uint8Array записывают через fs.writeFile() или преобразуют в Buffer для HTTP-фреймворка. В ответе следует установить Content-Type application/pdf и корректный Content-Disposition, если файл должен скачиваться под заданным именем.
import { readFile, writeFile } from 'node:fs/promises'
import { PDFDocument, StandardFonts } from 'pdf-lib'
const sourceBytes = await readFile('template.pdf')
const pdfDoc = await PDFDocument.load(sourceBytes)
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
const page = pdfDoc.getPage(0)
page.drawText('Order 184', { x: 48, y: 48, size: 11, font })
const outputBytes = await pdfDoc.save()
await writeFile('result.pdf', outputBytes)
Не передавайте в load() строковый путь, считая, что библиотека сама прочитает файл. Метод ожидает содержимое. Ошибка No PDF header found часто означает, что вместо PDF передали путь, HTML-страницу ошибки, JSON или пустой ответ. Перед загрузкой проверьте длину, первые байты и статус ответа. Настоящий PDF обычно начинается с маркера %PDF-, хотя перед ним иногда встречаются дополнительные байты.
В веб-маршруте устанавливайте предел размера входного файла до чтения в память. pdf-lib работает с байтами и объектами документа, поэтому крупный PDF может занять заметно больше своего дискового размера. При параллельной обработке нескольких запросов ограничьте конкурентность. Очередь с контролируемым числом задач обычно устойчивее, чем Promise.all() для сотен тяжёлых файлов.
Для повторяющегося шаблона не стоит разделять один изменяемый PDFDocument между запросами. Каждый заказ должен загружать чистые байты шаблона и создавать собственный объект. Иначе значения полей и страницы могут перейти в соседний результат. Сами неизменяемые байты шаблона можно кэшировать, но PDFDocument и полученные PDFPage, PDFFont или PDFImage следует считать принадлежащими конкретной операции.
Работа в браузере
В браузере исходный файл получают из input type=file через file.arrayBuffer(), из fetch() или из хранилища приложения. После изменения создают Blob, вызывают URL.createObjectURL() и назначают адрес iframe либо ссылке. Когда предпросмотр больше не нужен, URL.revokeObjectURL() освобождает связанный ресурс. Для большого документа не храните одновременно Base64, ArrayBuffer, Uint8Array и Blob без необходимости: каждая форма может увеличить пиковую память.
const file = fileInput.files[0]
const source = await file.arrayBuffer()
const pdfDoc = await PDFDocument.load(source)
applyChanges(pdfDoc)
const bytes = await pdfDoc.save()
const blob = new Blob([bytes], { type: 'application/pdf' })
const objectUrl = URL.createObjectURL(blob)
previewFrame.src = objectUrl
Запрос удалённого PDF зависит от CORS. Если сервер не разрешает доступ происхождению страницы, fetch() завершится ошибкой до того, как управление получит pdf-lib. Исправляется политика сервера, прокси на своём домене или предварительная загрузка пользователем. Режим no-cors не даёт читаемый массив байтов и не решает задачу. Также проверьте, что ответ действительно PDF, а не страница авторизации.
Длительная синхронная часть обработки может задержать интерфейс. Для больших файлов показывайте состояние до начала операции и отдавайте браузеру возможность перерисовать экран. При необходимости вынесите преобразование в Web Worker, передавая ArrayBuffer как transferable. Worker не отменяет потребление памяти, но не блокирует основной поток и делает кнопку отмены или прогресс приложения более отзывчивыми.
На мобильном браузере программное скачивание и открытие Blob ведут себя по-разному. Надёжнее дать пользователю явную кнопку после завершения, а не пытаться открыть окно из асинхронного обработчика без пользовательского жеста. Для встроенного приложения сохранение выполняют средствами файловой системы или общего доступа конкретной платформы, а pdf-lib отвечает только за формирование байтов.
Deno и React Native
В Deno библиотека используется в JavaScript-среде без нативных зависимостей, однако чтение и запись файлов подчиняются разрешениям Deno. Код должен получить разрешение на сеть, если исходные ресурсы загружаются, и на запись, если результат сохраняется. Импорт, файловые операции и выдача результата отличаются от Node.js, но PDFDocument, PDFPage и PDFForm применяются тем же образом.
В React Native основной нюанс — получение и сохранение бинарных данных. Некоторые сетевые и файловые библиотеки возвращают Base64, другие — ArrayBuffer или специальные структуры. Перед PDFDocument.load() преобразуйте данные в поддерживаемый формат, а после save() используйте API файловой системы приложения. Ошибки часто возникают не в PDF-операции, а на границе между Base64, строкой, Buffer-подобным объектом и Uint8Array.
Шрифты и изображения в мобильном пакете могут поставляться как ресурсы приложения. Их нужно прочитать именно как байты, а не как текст UTF-8. При внедрении крупного фото с камеры сначала уменьшите его размеры и уберите лишние метаданные. Ограниченная память устройства делает предварительную оптимизацию особенно важной. Проверяйте процесс на реальном устройстве с большим документом, а не только на эмуляторе и маленьком образце.
Ошибки загрузки и способы диагностики
Сообщение No PDF header found
Эта ошибка означает, что парсер не нашёл заголовок PDF в переданных данных. Выведите размер массива и первые несколько десятков байтов. Если там начинается HTML, сервер вернул страницу ошибки или форму входа. Если массив пуст, проблема в чтении файла. Если в переменной лежит строка Base64, убедитесь, что она не была повторно преобразована как обычный UTF-8-текст и не содержит лишнего префикса, который обработчик не ожидает.
EncryptedPDFError
Документ защищён шифрованием. Не пытайтесь считать ignoreEncryption полноценной расшифровкой. Получите разрешённую незашифрованную копию или обработайте файл инструментом, который понимает пароль и алгоритм шифрования. После расшифровки снова проверьте подписи и юридические ограничения: изменение криптографически подписанного документа обычно нарушает целостность подписи.
WinAnsi cannot encode
В тексте или значении формы присутствует символ вне кодировки стандартного шрифта. Подключите fontkit, внедрите TTF или OTF с нужными глифами и используйте его в drawText() либо updateFieldAppearances(). Не заменяйте кириллицу транслитерацией, если документ должен сохранять исходные данные. Дополнительно проверьте знак рубля, неразрывный пробел, длинное тире и кавычки: ошибка может быть вызвана не только буквами.
Поле не найдено или имеет другой тип
Сначала перечислите form.getFields(), для каждого выведите getName() и имя класса. Сравните с картой данных, учитывая регистр, пробелы и иерархические имена. Если поле называется правильно, но getTextField() выдаёт ошибку типа, примените соответствующий метод. Не подавляйте исключение и не продолжайте тихо: пустое обязательное поле в сформированном документе хуже заметной ошибки формирования.
Значение поля записано, но не видно
Обновите appearance stream через form.updateFieldAppearances() с подходящим шрифтом. Затем откройте файл в другом просмотрщике, чтобы отделить проблему документа от кэша интерфейса. Если планируется flatten(), выполняйте его только после обновления. Для старого шаблона проверьте виджеты поля и наличие корректной области: нулевая или вынесенная за страницу рамка не покажет значение.
Повреждённый или нестандартный объект
Некоторые генераторы создают PDF с отклонениями, которые просмотрщики терпят, а строгий парсер считает ошибкой. Сначала откройте файл в независимом валидаторе или пересохраните доверенным инструментом без изменения содержания. Не применяйте автоматическое “исправление” к единственной копии. Если проблема воспроизводится на минимальном образце, сохраните исходные байты и точный стек вызовов для диагностики.

Память, скорость и размер результата
Пиковая память включает исходные байты, разобранные объекты, внедрённые ресурсы и выходной массив. Небольшой файл с огромными сжатыми изображениями может оказаться тяжёлым после декодирования. Поэтому ограничение только по размеру загрузки недостаточно: учитывайте число страниц, размеры изображений и параллельность. В серверном процессе устанавливайте тайм-аут, предел входных данных и контролируемую очередь.
Повторное внедрение одинакового шрифта или картинки увеличивает файл. Вынесите embedFont(), embedPng() и embedJpg() за цикл страниц. При копировании страниц из нескольких документов одинаковые ресурсы не всегда дедуплицируются так, как ожидает пользователь, поэтому итоговый размер может быть больше суммы видимого содержимого. После функциональной проверки измерьте файл на реальном наборе данных и при необходимости добавьте отдельный этап оптимизации.
useObjectStreams в параметрах save() влияет на способ сериализации объектов и совместимость с очень старыми программами. Значение по умолчанию подходит большинству современных просмотрщиков. Отключение может упростить диагностику и повысить совместимость с отдельной старой системой, но увеличивает размер. Менять параметр следует по результатам конкретного теста, а не как универсальное ускорение.
objectsPerTick управляет тем, сколько объектов обрабатывается между уступками циклу событий при сохранении. Настройка может влиять на отзывчивость в браузере, но не устраняет общий объём работы. Слишком малое значение увеличит число переключений, слишком большое дольше блокирует поток. Для обычных документов оставьте стандартное значение; настройку имеет смысл измерять только на стабильном тестовом наборе.
Для пакетной обработки записывайте метрики: размер входа и выхода, число страниц, время load(), время преобразования, время save() и пиковую память процесса. Эти данные быстро показывают, проблема находится в разборе, собственном алгоритме или сериализации. Без измерений легко оптимизировать короткий вызов и не заметить, что основное время тратится на загрузку шрифта, сеть или предварительную обработку изображений.
Практический сценарий: штамп и номер на каждой странице
Сначала загрузите исходный PDF, внедрите один шрифт и при необходимости один логотип. Получите pages = pdfDoc.getPages() и пройдите массив. Для каждой страницы прочитайте width и height, вычислите положение относительно правого нижнего угла и нарисуйте полупрозрачную подложку, затем номер и дату. Такой расчёт работает для разных размеров лучше, чем абсолютные координаты, но поворотные страницы надо обрабатывать отдельно.
- Проверить CropBox и rotation каждого листа.
- Внедрить шрифт и изображение один раз до цикла.
- Использовать номер пользователя i + 1, а индекс страницы оставить i.
- Не закрывать область подписи или машиночитаемую метку.
- Открыть итог в нескольких просмотрщиках и проверить печать.
Если штамп должен быть под исходным содержимым, высокоуровневый порядок команд может не дать желаемого результата на существующей странице, потому что новые операции добавляются к потоку. Более предсказуемый способ — создать новый лист, внедрить исходную страницу как PDFEmbeddedPage, нарисовать фон или водяной знак, затем drawPage() с исходной страницей и поверх добавить служебный номер. При этом аннотации и интерактивность исходника могут не сохраниться, поэтому метод выбирают исходя из требований.
Практический сценарий: заполнение шаблона заказа
Шаблон должен содержать стабильные AcroForm-поля. При запуске загрузите чистые байты, получите форму, сравните фактический набор полей с ожидаемым и только затем применяйте данные. Для строк нормализуйте переносы, для списков проверяйте значение по getOptions(), для флажков используйте явное логическое правило. Не передавайте undefined во все поля подряд: различайте отсутствие данных, пустую строку и намеренное очищение.
- Загрузить чистый шаблон и проверить число страниц.
- Получить getFields() и сверить имена с контрактом данных.
- Заполнить текстовые поля, флажки, списки и радиогруппы.
- Внедрить шрифт с кириллицей и обновить внешний вид.
- Проверить обязательные значения программно.
- При необходимости сплющить форму.
- Установить свойства документа и сохранить результат.
В тестах проверяйте не только отсутствие исключения. Повторно загрузите полученные байты до flatten() и прочитайте значения полей. После flatten() убедитесь, что getFields() пуст или не содержит обработанных полей, а визуальный рендер сохранил текст. Отдельно протестируйте длинное имя, максимальную сумму, пустое необязательное поле, кириллицу и символы валют.
Практический сценарий: объединение выбранных страниц
Пользователь может указать диапазоны 1-3, 7, 10-12. Сначала разберите строку в массив пользовательских номеров, удалите дубликаты по правилам задачи и проверьте границы для каждого документа. Затем преобразуйте номера в нулевые индексы и передайте copyPages(). Не допускайте отрицательных значений и тихого пропуска страниц: покажите понятную ошибку с именем файла и допустимым диапазоном.
Если объединяются заполненные формы, определите, нужна ли интерактивность. Одинаковые имена полей в разных документах могут связывать значения или конфликтовать. Для финального комплекта заполните и сплющите каждую форму отдельно, после чего копируйте страницы. Для файла, который должен оставаться редактируемым, нужен отдельный тест структуры AcroForm и, возможно, более специализированный инструмент.
После объединения задайте новый заголовок и при необходимости очистите свойства, относящиеся к отдельным документам. Закладки и оглавление не создаются автоматически из имён файлов. Если получателю нужна навигация, добавьте видимую страницу содержания или используйте компонент, умеющий строить outline. Простого порядка страниц достаточно не для каждого делового комплекта.
Практический сценарий: отчёт с таблицами
pdf-lib даёт примитивы, но не готовый табличный макет. Создайте модель столбцов с x, width, alignment и функцией форматирования. Для каждой ячейки перенесите текст по ширине, определите число строк и высоту строки. Высота ряда равна максимальной высоте его ячеек плюс внутренние отступы. Перед рисованием сравните будущую нижнюю координату с полем страницы; если ряд не помещается, добавьте лист и повторите шапку.
Сначала рисуйте фон и границы ряда, затем текст. Числа выравнивайте вправо, заголовки — по принятому шаблону. Не рассчитывайте ширину пробелами: используйте widthOfTextAtSize(). Для длинного слова без пробелов предусмотрите принудительное деление или уменьшение шрифта в разумном диапазоне. Нельзя позволять строке выходить за рамку и закрывать соседний столбец.
Итоги и подписи лучше держать как отдельные блоки, чтобы переносить их целиком на следующий лист. Если подпись должна следовать сразу после таблицы, заранее вычислите требуемую высоту блока. Колонтитулы рисуйте на каждой странице одним и тем же помощником. Такой макет требует больше кода, чем HTML, но даёт точные координаты и не зависит от браузерного движка.
Проверка результата
Минимальная автоматическая проверка повторно загружает результат через PDFDocument.load(), сверяет число страниц, размеры и свойства. Для формы до сплющивания читаются значения полей. Для сборки проверяется ожидаемый порядок страниц по нанесённым маркерам или контрольным данным. Такой тест ловит пустой файл, ошибочный диапазон и незаполненное поле, но не заменяет визуальный контроль.
Визуальные регрессионные тесты рендерят страницы в изображения внешним движком и сравнивают с эталоном с допустимым порогом. Они находят сдвиг текста, пропавший шрифт, неправильный перенос и пустой appearance stream. Эталон надо обновлять осознанно, просматривая различия. Сравнение байтов не подходит для многих PDF, потому что даты, порядок объектов и служебные идентификаторы могут меняться без видимого отличия.
Официальный проект проверяет результаты в нескольких распространённых просмотрщиках; тот же принцип полезен и в прикладной системе. Минимум откройте контрольные файлы во встроенном просмотрщике браузера и в отдельной программе. Для печатных документов добавьте проверку физической печати или надёжного растра. Особое внимание уделите прозрачности, встроенным шрифтам, полям формы, вложениям и нестандартным размерам страниц.

Валидатор PDF может сообщить о структурных проблемах, которые визуально не заметны. Его полезно запускать для типовых шаблонов и после изменения процесса. При этом формальная валидность не гарантирует, что документ соответствует бизнес-требованию: правильное число страниц, значения, порядок и читаемость проверяются отдельно. Лучший набор тестов сочетает структурную проверку, повторную загрузку, визуальный рендер и ручной просмотр крайних случаев.
Безопасность входных данных
PDF является сложным контейнером, поэтому пользовательский файл следует считать недоверенным. Ограничьте размер, время обработки и конкурентность. Не сохраняйте результат под именем, напрямую полученным из запроса, без очистки пути. Не передавайте произвольные вложения и метаданные дальше, если они не нужны. Журналируйте технические характеристики, но не записывайте чувствительные значения полей в общий лог.
Наложение белого прямоугольника не удаляет скрытый текст, а flatten() формы не гарантирует очистку всех исторических данных и вложений. Перед публикацией конфиденциального документа используйте специализированную редакцию и проверку. Аналогично, нарисованная подпись или печать является изображением, а не доказательством целостности. Для криптографической подписи нужен процесс с сертификатами, контейнером подписи и проверкой доверия.
При загрузке удалённых ресурсов проверяйте разрешённые домены и не позволяйте пользовательскому URL превращать сервер в универсальный прокси. Лучше получать шрифты, логотипы и шаблоны из контролируемого хранилища. Если файл приходит из внешней системы, фиксируйте статус ответа и MIME-тип до передачи в PDFDocument.load(). Это уменьшает число неясных ошибок и риск обработки неожиданного содержимого.
Организация кода вокруг pdf-lib
Разделите получение данных, преобразование PDF и доставку результата. Функция преобразования должна принимать байты и нормализованную модель данных, а возвращать байты или объект с диагностикой. Она не должна сама читать HTTP-запрос, обращаться к базе и отправлять ответ. Такое разделение позволяет прогнать одну и ту же логику в тесте, очереди и интерактивном маршруте.
Вынесите повторяющиеся операции в небольшие функции: loadTemplate(), embedCorporateFonts(), drawHeader(), drawFooter(), fillOrderForm(), copySelectedPages(), validateResult(). Координаты и имена полей храните в конфигурации конкретного шаблона. Не создавайте один огромный обработчик с десятками чисел: при изменении макета трудно понять, какая координата относится к какому блоку.
Ошибки делите на входные, шаблонные и внутренние. Входная ошибка сообщает пользователю, что файл защищён, слишком велик или имеет неподдерживаемый формат. Шаблонная означает отсутствующее поле, неожиданный размер страницы или неправильный шрифт и требует исправления конфигурации. Внутренняя фиксирует стек и идентификатор операции, но не раскрывает служебные детали в публичном ответе.
Для повторяемости закрепляйте зависимости проекта и тестируйте изменения на наборе реальных PDF. Документы, созданные разными программами, отличаются структурой сильнее, чем кажется по внешнему виду. Один простой образец не покрывает формы, повернутые страницы, встроенные шрифты, большие изображения и повреждённые объекты. Набор тестов должен включать каждый тип, который поступает в рабочем процессе.
Параметры загрузки и сохранения
PDFDocument.load() имеет параметры, которые полезны не в каждом проекте, но важны при диагностике сложных файлов. updateMetadata управляет автоматическим обновлением служебных свойств. ignoreEncryption отключает ранний отказ для защищённого документа, однако не выполняет расшифровку. throwOnInvalidObject определяет, должна ли встреча с некорректным объектом немедленно завершить разбор или парсер попробует продолжить. Такие послабления нельзя включать глобально без тестов: файл может загрузиться, но потерять часть структуры.
Параметр parseSpeed задаёт стратегию уступок циклу событий во время разбора. Он полезен в интерактивной среде, где длинная операция не должна надолго блокировать интерфейс, но не превращает тяжёлый PDF в потоковый документ и не уменьшает обязательную работу парсера. Значение выбирают по измерениям на реальных файлах. Для серверной очереди обычно важнее общая пропускная способность, а для браузерной формы — отсутствие длительного зависания основного потока.
После загрузки можно вызвать flush(), чтобы подготовить внедрённые ресурсы к сериализации, но обычный save() сам выполняет необходимые действия. Ручной flush полезен для специальных интеграций и диагностики, а не как обязательный шаг. Повторные вызовы save() создают новые массивы байтов; если приложение сохраняет предварительный просмотр и финальный файл, оно должно явно освобождать старые Blob-адреса и не держать все результаты в памяти.
В параметрах save() доступны useObjectStreams, addDefaultPage, objectsPerTick и updateFieldAppearances. addDefaultPage предотвращает создание формально пустого документа без страниц, когда включён соответствующий режим. updateFieldAppearances позволяет управлять автоматическим обновлением изменённых полей перед сериализацией. Если внешний вид уже сформирован собственным кодом, настройку надо проверять особенно внимательно, чтобы стандартное обновление не перезаписало ожидаемое оформление.
При воспроизводимом формировании полезно явно задавать метаданные и избегать случайных входных значений. Даже при одинаковом внешнем виде байты могут различаться из-за дат, идентификаторов и порядка объектов. Поэтому тест бизнес-логики должен сравнивать структуру и рендер, а не требовать полного совпадения хеша, если процесс сознательно меняет время создания. Хеш удобен для контроля неизменности конкретного выданного файла, но не всегда для регрессии генератора.
Стандартные размеры и собственные шаблоны страниц
addPage() может принять готовый PDFPage, массив размеров или не получить аргументов. Для форматов A4, Letter и других удобно завести именованные константы ширины и высоты в пунктах. Не смешивайте миллиметры и пункты в одном расчёте: используйте функцию перевода millimeters * 72 / 25.4. При округлении оставляйте дробные значения, потому что точный A4 равен приблизительно 595.28 на 841.89 пункта.
Поля страницы лучше задавать объектом layout: top, right, bottom, left. Тогда доступная ширина вычисляется как pageWidth - left - right, а нижний предел — bottom. Все функции рисования получают layout, а не знают случайные числа. Такой подход облегчает смену формата и предотвращает ситуации, когда заголовок использует отступ 40, таблица 48, а подпись 36 без объяснения.
При создании альбомного листа можно сразу поменять местами ширину и высоту. Поворот словаря страницы — другой механизм и может усложнить расчёты. Для нового отчёта проще добавить страницу нужной ориентации без rotation. При работе с чужим файлом ориентацию приходится определять по сочетанию размеров и getRotation(), потому что визуально альбомный лист может иметь книжный MediaBox и поворот 90 градусов.
Шаблонные координаты полезно хранить рядом с контрольным изображением страницы и обозначать смысловыми именами: customerNameBox, totalBox, stampAnchor. Для каждой области фиксируют x, y, width, height и допустимый размер текста. Тогда изменение макета требует обновить конфигурацию, а не искать число 417 в коде. Перед публикацией шаблона автоматический тест может проверить, что все области лежат внутри CropBox.
Типизация и проверяемые контракты данных
TypeScript помогает отделить допустимые данные от произвольного объекта. Для формы заказа можно описать интерфейс с обязательными строками, перечислением способа доставки и логическим флагом. Функция заполнения принимает уже проверенную структуру, а не разбирает значения на ходу. Это снижает вероятность передать число в setText(), неизвестный вариант в select() или пропустить обязательное поле.
Имена полей удобно собирать в объект с литеральными типами. Тогда опечатка обнаруживается при сборке, а изменение шаблона видно в одном месте. Сопоставление может хранить имя поля, тип, функцию форматирования и обязательность. До заполнения код сравнивает фактические поля PDF с этой схемой и формирует понятный список расхождений. Такой контроль особенно полезен, когда дизайнер периодически экспортирует новый шаблон.
Внутренние функции можно типизировать так, чтобы drawHeader() принимала PDFPage и набор уже внедрённых ресурсов, а не весь контекст приложения. Ресурсы описываются объектом с PDFFont и PDFImage. Это предотвращает случайное повторное внедрение и делает зависимость функции явной. Возвращаемое значение обычно не требуется, потому что методы изменяют страницу, но функция может вернуть новую координату y для последующего блока.
Для ошибок полезен собственный тип с кодом: INVALID_INPUT, ENCRYPTED_PDF, TEMPLATE_MISMATCH, UNSUPPORTED_FORM, SAVE_FAILED. В пользовательском интерфейсе код преобразуется в понятное сообщение, а журнал получает техническую причину и идентификатор операции. Не следует показывать пользователю полный stack trace или внутренние пути. В тестах, напротив, код ошибки позволяет точно проверить ожидаемую ветвь.
Типизация не проверяет содержимое данных во время выполнения. Значения из JSON, формы и внешнего API надо валидировать до передачи в PDF-слой. Проверка включает длину строк, допустимые перечисления, формат дат, пределы чисел и отсутствие управляющих символов. После валидации форматирование валюты и даты выполняется централизованно, чтобы одинаковые значения не выглядели по-разному в разных частях документа.
Работа с вложениями и переносимость файла
attach() помещает данные во внутреннее дерево имён PDF и создаёт файловую спецификацию. Имя вложения, MIME-тип, описание, даты создания и изменения помогают совместимому просмотрщику показать его пользователю. Сам факт успешного attach() не означает, что панель вложений будет видна во встроенном браузерном просмотрщике. Для проверки откройте файл в программе, которая явно отображает прикреплённые файлы.
Имя вложения должно быть безопасным и понятным. Не переносите путь с устройства пользователя и не допускайте сегменты каталогов. Если два вложения получают одинаковое имя, поведение надо проверить и лучше заранее обеспечить уникальность. Для связанного набора данных используйте стабильное имя, например order-data.json, и фиксируйте формат содержимого вне PDF, чтобы получатель мог его обработать.
Большое вложение увеличивает PDF почти на свой полный размер и может пройти незаметно для визуальной проверки. Перед добавлением установите предел и покажите размер в диагностике. Если вложение является обязательной частью обмена, проверьте его извлечение после сохранения внешним инструментом. pdf-lib предоставляет добавление, но не должна быть единственным доказательством, что конкретная система получателя умеет получить файл.
При объединении документов не предполагайте, что вложения исходных документов автоматически переходят вместе с copied pages. Страница и глобальная структура файла — разные уровни. Если вложения должны сохраниться, их нужно извлечь и добавить в целевой документ поддерживаемым способом либо выбрать инструмент, который умеет переносить соответствующее дерево. Простое копирование видимых страниц решает только задачу страниц.
Нестандартные формы и границы автоматизации
Поле может иметь несколько виджетов на разных страницах. Изменение значения относится к полю, а внешний вид обновляется для каждого виджета. Это полезно для повторяющегося номера договора, но усложняет нестандартное оформление. Если один виджет повреждён или находится за границей страницы, значение будет корректным на уровне формы, а визуальная проблема останется только в одном месте. Диагностика должна учитывать число виджетов.
Флаги readOnly, required и export управляют поведением совместимых просмотрщиков и экспортом формы. Они не заменяют серверную авторизацию и валидацию. Пользователь может открыть PDF программой, которая игнорирует часть ограничений, или изменить файл другими средствами. Поэтому обязательность и право изменения проверяются в приложении, а PDF-флаги используются как помощь интерфейсу получателя.
Раскрывающийся список может быть редактируемым и допускать значение, которого нет среди опций. Метод select() при выборе неизвестного значения способен включить редактирование. Если бизнес-процесс допускает только фиксированный набор, сначала сравните строку с getOptions() и отклоните неизвестное значение. Для множественного выбора лучше использовать option list, поскольку просмотрщики обычно показывают только одно выбранное значение раскрывающегося списка.
Подписи и кнопки формы часто зависят от JavaScript-действий Acrobat. Высокоуровневый API pdf-lib не является конструктором таких сценариев. Если шаблон рассчитывает на вычисления, проверку и динамическое появление областей внутри просмотрщика, автоматическое заполнение может дать статический результат без ожидаемой логики. Надёжнее вычислить значения в приложении, записать их явно и при необходимости сплющить форму.
Перед промышленным использованием формы создайте карту полей: имя, тип, страницы, число виджетов, доступные варианты, максимальная длина и поддержка кириллицы. Карта превращает неизвестный PDF в проверяемый контракт. При получении нового шаблона скрипт сравнивает карту и сообщает изменения. Это предотвращает тихие ошибки, когда поле было удалено, получило другое имя или превратилось из text field в dropdown.
Подготовка исходных PDF для стабильной обработки
Лучший исходник — файл, созданный предсказуемым экспортом и проверенный на наборе операций. Если документы приходят от разных поставщиков, полезен этап нормализации внешним инструментом: исправление структуры, снятие разрешённого шифрования, приведение поворотов и повторное сохранение. Нормализация не должна менять визуальный смысл и выполняется только на копии. После неё сравнивают число страниц и рендер.
Для шаблонов избегайте лишних встроенных ресурсов и огромных изображений. Пустой бланк с фотографией высокого разрешения будет копироваться в каждый результат и увеличит нагрузку. Оптимизируйте фон до разумного разрешения, внедрите необходимые шрифты и удалите неиспользуемые страницы. Чем проще и стабильнее шаблон, тем легче диагностировать различия между данными и структурой PDF.
Если документ поступает после сканирования, исправьте ориентацию и размер страниц до нанесения координатных отметок. Сканер может создавать каждый лист с немного разным CropBox и поворотом. Для автоматической штамповки либо нормализуйте геометрию, либо вычисляйте положение для каждого листа отдельно. Проверка только первой страницы не гарантирует правильное положение на остальных.
Сохраните небольшой набор эталонных файлов: чистый шаблон, заполненный результат, пример с максимальными строками, смешанная ориентация, форма с кириллицей и большой многостраничный документ. Эти образцы должны быть обезличены, но структурно повторять рабочие случаи. Они используются при изменении зависимостей, среды выполнения и кода макета, чтобы обнаружить регрессию до выдачи реальных документов.
Сравнение pdf-lib с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| pdf-lib | Изменения готовых PDF, форм и страниц из JavaScript-кода | Нет визуального редактора и HTML/CSS-макета |
| PDF Commander | Ручного редактирования, сборки и оформления PDF через понятные инструменты | Не предназначен для встраивания как JavaScript API |
| jsPDF | Формирования новых документов непосредственно в веб-сценарии | Работа с произвольным существующим PDF менее центральна |
| PDFKit | Программной генерации отчётов, текста и векторной графики | Ориентирован прежде всего на создание новых документов |
| PDF.js | Отображения PDF и построения собственного просмотрщика | Не является API для авторского изменения и сохранения PDF |
| Puppeteer | Печати сложного HTML/CSS-макета средствами браузерного движка | Требует браузерной среды и не заменяет объектное редактирование PDF |
pdf-lib выбирают, когда приложение уже работает на JavaScript и должно программно загрузить PDF, добавить или переставить страницы, заполнить AcroForm и сохранить байты. PDF Commander удобнее для человека, которому надо открыть файл и выполнить правку вручную. jsPDF и PDFKit рациональны, когда документ в основном создаётся с нуля. PDF.js нужен для просмотра, а Puppeteer — когда исходный макет описан HTML и CSS; после печати браузером pdf-lib может выполнить заключительную сборку.
Когда выбор pdf-lib оправдан
Библиотека особенно полезна для предсказуемых операций над известными шаблонами: заполнить поля договора, нанести номер, объединить выбранные страницы, добавить логотип, вложить исходные данные и установить свойства. В таких задачах явные методы и байтовый результат хорошо сочетаются с серверным маршрутом, очередью, тестами и браузерной формой. Отсутствие скрытого состояния делает преобразование воспроизводимым.
Она менее удобна, когда пользователь ожидает мышью выделять существующий текст, видеть панели инструментов, применять OCR, редактировать скан, строить HTML-макет или ставить криптографическую подпись. Попытка реализовать эти функции поверх низкоуровневого рисования быстро превращается в отдельный редактор. В таком случае лучше выбрать визуальную программу или специализированный движок, а pdf-lib оставить для тех завершающих операций, которые она выполняет точно.
Перед внедрением возьмите несколько самых сложных реальных файлов и выполните полный путь: загрузка, изменение, сохранение, повторное открытие, просмотр и печать. Проверьте кириллицу, повороты, формы, изображения и большой объём. Если этот набор проходит без ручных исправлений, дальнейшее масштабирование обычно сводится к контролю памяти, очереди и качеству данных. Если проблемы возникают уже на шаблонах, их дешевле решить до интеграции с пользовательским интерфейсом.
Итоговая схема надёжного процесса
Надёжная схема начинается с проверки входных байтов и ограничений, затем создаётся отдельный PDFDocument для одной операции. Ресурсы внедряются один раз, координаты рассчитываются из размеров страниц, поля сверяются по именам и типам, а кириллица получает пользовательский шрифт. После всех изменений выполняется один save(), результат повторно загружается для структурной проверки и проходит визуальный контроль на крайних примерах.
Данные и шаблон следует хранить отдельно от производного PDF. Это позволяет заново сформировать документ после исправления значения, изменить оформление без ручной правки готовых файлов и доказуемо повторить процесс. Для задач, выходящих за возможности API, добавляются отдельные этапы: OCR до наложения, браузерная печать до сборки, безвозвратное удаление конфиденциальных данных и подпись после формирования.
При таком разделении pdf-lib выполняет именно ту роль, в которой она наиболее предсказуема: управляет объектами PDF из кода, создаёт и изменяет страницы, рисует содержимое, работает с AcroForm, переносит ресурсы и возвращает готовые байты. Пользовательский экран, хранение, очередь, проверка и доставка остаются под контролем приложения, а ограничения библиотеки учитываются в архитектуре заранее, а не обнаруживаются после выпуска документов.