HummusJS позволяет из Node.js создавать PDF с текстом, изображениями и векторной графикой, дописывать содержимое на существующие страницы, объединять документы, копировать отдельные объекты и разбирать внутреннюю структуру файла через программный API.
Рабочий процесс строится вокруг объекта PDFWriter: код открывает файл или поток, создаёт страницу с заданным MediaBox, получает контекст содержимого, последовательно размещает элементы и завершает страницу методом writePage. Для изменения готового документа применяется writer для модификации и PDFPageModifier, а для чтения — PDFReader с доступом к страницам, словарям, таблице перекрёстных ссылок и потокам объектов.
У HummusJS нет визуальной панели с миниатюрами и кнопками: результат определяется JavaScript-кодом, координатами в пунктах, параметрами шрифта, матрицами преобразования и порядком записи PDF-объектов. Такой подход удобен для серверной генерации счетов, пакетной простановки штампов, сборки приложений из нескольких файлов и других повторяемых операций, но требует аккуратно управлять ресурсами страницы и проверять выходной PDF в нескольких просмотрщиках.
Скачать HummusJS
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет графического интерфейса
- Нужны знания PDF API
- Зависит от нативной сборки
Как устроен рабочий процесс HummusJS
Главный объект сценария создаётся вызовом createWriter. В файловом варианте ему передают путь назначения, а в потоковом — объект, который умеет принимать массивы байтов и сообщать текущую позицию. Writer накапливает структуру документа, регистрирует шрифты и изображения, создаёт косвенные объекты, управляет ресурсами страниц и в конце записывает таблицу перекрёстных ссылок. Преждевременное завершение процесса до вызова end оставляет файл без корректного финала, поэтому завершение writer должно находиться в контролируемой ветке кода, а ошибки подготовки данных следует обрабатывать до начала необратимой записи.

Страница создаётся не по названию формата бумаги, а по четырём координатам прямоугольника. Для A4 обычно используют MediaBox 0, 0, 595, 842 с небольшими вариациями округления, а для Letter — 0, 0, 612, 792. Нулевая точка расположена внизу слева, поэтому привычная веб-разработчику координата сверху требует пересчёта: вертикальное положение строки получают вычитанием верхнего отступа и высоты блока из высоты страницы. Это особенно важно при переносе макета из HTML или дизайна, где ось Y направлена вниз.

Контекст страницы создаётся методом startPageContentContext. Его методы возвращают тот же контекст, поэтому команды можно объединять в цепочки. В одной цепочке допустимо нарисовать фон, рамки, текст и изображения, однако ресурсоёмкие операции создания XObject иногда требуют приостановить активный контекст. Если библиотека сообщает об активном контексте или записывает ресурс в неожиданном месте, контекст приостанавливают методом pausePageContentContext, создают ресурс и затем продолжают рисование.

const hummus = require('hummus');
const writer = hummus.createWriter('result.pdf', { compress: true });
const page = writer.createPage(0, 0, 595, 842);
const ctx = writer.startPageContentContext(page);
ctx.writeText('Документ сформирован', 72, 770, {
font: writer.getFontForFile('./fonts/DejaVuSans.ttf'),
size: 14,
colorspace: 'gray',
color: 0x00
});
writer.writePage(page);
writer.end();
В примере сначала создаётся writer, затем страница и её контекст. Шрифт загружается из файла, а строка помещается по базовой линии на координатах 72 и 770. После завершения контента writePage фиксирует страницу в дереве Pages, а end закрывает документ. Практически полезно вынести размеры бумаги, поля, пути к шрифтам и стили в конфигурацию, чтобы шаблон не зависел от разбросанных по коду чисел.
Страницы, поля и система координат
HummusJS работает с геометрией PDF напрямую. MediaBox задаёт физическую область страницы, CropBox — видимую область, TrimBox — предполагаемую линию обреза, BleedBox — выпуск под обрез, а ArtBox — содержательную область. При создании простого документа достаточно MediaBox, но при копировании страниц или подготовке печатных материалов выбор бокса влияет на масштаб и видимую часть источника. Если вставленная страница неожиданно обрезана, необходимо проверить, какой бокс выбран при создании Form XObject.
Все размеры выражаются в пунктах, где 72 пункта соответствуют одному дюйму. Миллиметры переводятся формулой mm × 72 / 25,4. Для шаблона с точными полями полезно сделать функцию преобразования и использовать её для каждой координаты. Отдельные округления лучше выполнять только в момент передачи значения API: накопление округлённых координат в таблице или сетке может дать заметный сдвиг на последней строке.
Положение текста определяется базовой линией, а не верхней границей глифа. Поэтому одинаковая координата Y для разных гарнитур может дать различный визуальный отступ сверху. Для выравнивания по верхнему краю следует учитывать метрики шрифта или заранее измерять тестовую строку. Для нижних колонтитулов полезно считать позицию от нижней границы страницы, а для верхних блоков — от высоты MediaBox; смешивание этих двух подходов без общей функции координат быстро приводит к ошибкам.
Поворот страницы и поворот содержимого — разные операции. Если требуется разместить блок под углом, используют матрицу cm внутри сохранённого графического состояния q/Q. Если же нужно изменить геометрию готовой страницы, придётся работать со словарём страницы и её параметрами поворота либо создать новую страницу нужной ориентации и перенести содержимое. Простое изменение ширины и высоты не вращает существующие координаты автоматически.

Текст, шрифты и измерение строк
Метод writeText принимает строку, координаты и объект параметров. В нём задаются объект шрифта, размер, цветовое пространство, цвет и подчёркивание. Для серого цвета достаточно одного компонента, для RGB — трёх, для CMYK — четырёх. Числовая запись цвета удобна для постоянных стилей, но её стоит оборачивать в функции rgb или cmyk, чтобы код не превращался в набор трудно читаемых шестнадцатеричных значений.
Шрифт получают методом getFontForFile. Документация перечисляет TrueType, OpenType, Type 1, dfont и TTC; для коллекций можно указывать индекс нужной гарнитуры, а для Type 1 передают связанные файлы PFB и PFM. Файл шрифта должен быть доступен процессу в момент генерации. Системный путь, работающий на компьютере разработчика, часто отсутствует в контейнере или на сервере, поэтому надёжнее хранить лицензированный шрифт вместе с приложением и строить путь относительно каталога проекта.
Русский текст требует гарнитуры с кириллическими глифами. Наличие файла с расширением TTF ещё не гарантирует поддержку нужных символов: некоторые декоративные наборы содержат только латиницу. Перед массовой генерацией следует сформировать контрольный PDF со строчными и прописными буквами, цифрами, знаком рубля, тире, кавычками и типичными фамилиями. Пустые квадраты или пропавшие символы указывают не на кодировку JavaScript, а чаще всего на отсутствие глифов в выбранном шрифте.
Автоматического переноса абзаца по ширине в низкоуровневом вызове нет. Сценарий разбивает текст на слова, измеряет кандидатную строку метриками шрифта и переносит слово, когда ширина превышает колонку. Межстрочный интервал рассчитывают отдельно. Для выравнивания справа из правой координаты вычитают измеренную ширину строки, а для центрирования — половину разницы между шириной блока и строки. Такой алгоритм должен учитывать неразрывные значения, длинные номера и строки без пробелов.

function drawRightAligned(ctx, writer, text, right, y, font, size) {
const width = font.calculateTextDimensions(text, size).width;
ctx.writeText(text, right - width, y, {
font, size, colorspace: 'gray', color: 0x00
});
}
Конкретное имя метода измерения зависит от объекта шрифта, поэтому перед внедрением вспомогательной функции следует сверить доступные методы в используемой сборке и типовых объявлениях. Смысл остаётся одинаковым: сначала получить ширину набора глифов при заданном размере, затем вычислить X. Не следует выравнивать денежные суммы количеством пробелов: пробелы имеют собственную ширину, шрифт может быть пропорциональным, а просмотрщик способен применять отличающиеся метрики.
Изображения: JPG, PNG, TIFF и страницы PDF
Метод drawImage помещает изображение по нижнему левому углу. Он принимает путь к JPG, PNG, TIFF или PDF и необязательный объект параметров. Для многостраничного TIFF и PDF индекс выбирает нужную страницу. Матрица из шести чисел даёт полный контроль над масштабированием, поворотом и переносом, а объект transformation с width и height позволяет вписать изображение в прямоугольник. Параметр proportional сохраняет пропорции, иначе изображение растягивается независимо по двум осям.
PNG удобен для логотипов и печатей с прозрачным фоном. JPEG подходит для фотографий, но повторное сохранение уже сжатого изображения может ухудшить качество ещё до помещения в PDF. TIFF полезен в сканирующих процессах, включая многостраничные файлы. Для чёрно-белого TIFF доступны варианты использования как маски, где единичные биты окрашиваются заданным цветом, а нулевые становятся прозрачными. Градации серого можно использовать как карту перехода между двумя цветами.
Размеры исходного изображения можно получить до рисования. Это позволяет вычислить рамку, выбрать масштаб без увеличения маленькой картинки и разместить подпись строго под фактической высотой. Если изображение должно только уменьшаться при переполнении, применяется режим overflow; если оно всегда должно заполнять целевой размер, используется режим always. Сохранение пропорций вместе с ограничением по ширине и высоте обычно оставляет свободное место по одной оси, которое нужно распределить вручную для центрирования.
Для многократного размещения одного ресурса выгодно создать XObject и вызывать doXObject. JPEG можно превратить в image XObject или form XObject, PNG и TIFF — в reusable form. Ресурс создаётся один раз, а затем используется на каждой странице с разными матрицами. Это уменьшает дублирование данных и ускоряет генерацию длинных документов с одинаковым логотипом, водяным знаком или элементом фирменного бланка.

const ctx = writer.startPageContentContext(page);
ctx.drawImage(72, 520, './assets/photo.jpg', {
transformation: {
width: 220,
height: 160,
proportional: true,
fit: 'overflow'
}
});
Если drawImage не принимает данные из памяти, можно перейти к форматно-специфичным методам, поддерживающим пользовательский поток. Такой поток должен быть позиционируемым: декодеру недостаточно однократного последовательного чтения, ему требуется перемещаться по файлу. Для буфера реализуют read, notEnded, setPosition, setPositionFromEnd, skip и getCurrentPosition либо используют готовый адаптер из пакета. Ошибки позиционирования проявляются как повреждённая картинка, неожиданное завершение чтения или отказ декодера.
Векторная графика и низкоуровневые операторы
Высокоуровневые методы контекста рисуют прямоугольники, квадраты, окружности и пути. Для каждого примитива задаются режим stroke, fill или сочетание, толщина линии и цвет. Путь подходит для ломаных, разделителей таблицы, стрелок и нестандартных контуров. Поскольку PDF хранит векторные команды, линии сохраняют резкость при масштабировании и обычно занимают меньше места, чем растровая картинка такой же формы.
Когда готового метода недостаточно, контекст предоставляет методы с именами PDF-операторов. q сохраняет графическое состояние, Q восстанавливает его, cm умножает текущую матрицу преобразования, doXObject выводит ранее созданный ресурс. Текстовые операторы позволяют управлять текстовой матрицей и отдельными глифами. Такой уровень полезен для наложения страниц, штрихкодов, нестандартных режимов окрашивания и точного повторения структуры существующего PDF.
Пара q/Q должна быть сбалансирована. Если забыть Q, масштаб, поворот, цвет или толщина линии продолжат действовать на последующие элементы. Ошибка особенно заметна в длинной цепочке, где один логотип внезапно уменьшает весь нижний колонтитул. Хорошая практика — оформлять каждую независимую трансформацию отдельным блоком q().cm(...), выполнять рисование и сразу вызывать Q(), не смешивая внутри него несвязанные элементы.
Матрица cm содержит шесть чисел a, b, c, d, e, f. Первые четыре определяют масштаб, поворот и сдвиг осей, последние два — перенос. Для простого масштаба используются a и d, для поворота — синус и косинус угла, для размещения — e и f. Если страница источника имеет собственный CropBox не от нуля, матрица должна компенсировать его начало, иначе вставленный фрагмент сместится относительно ожидаемой рамки.

Повторно используемые Form XObject
Form XObject — самостоятельный графический объект с собственным ограничивающим прямоугольником и ресурсами. В HummusJS его можно создать, нарисовать внутри один раз и затем размещать на разных страницах. Подход подходит для шапки, подвала, штампа, фоновой сетки, блока реквизитов и других повторяющихся частей. При размещении форма ведёт себя как единый объект, поэтому её легко масштабировать или поворачивать общей матрицей.
Преимущество формы проявляется не только в размере файла. Она отделяет подготовку шаблона от наполнения страницы данными. Код сначала создаёт графический компонент, затем функция страницы размещает его и добавляет переменные строки. Это снижает риск случайно изменить фирменную часть при переработке бизнес-логики. Форму можно передавать между функциями как идентификатор ресурса, не раскрывая внутреннюю последовательность операторов.
Ограничивающий прямоугольник формы должен покрывать всё содержимое. Элементы за его пределами могут быть обрезаны просмотрщиком. Если форма выглядит неполной, сначала проверяют box, затем матрицу размещения и только потом исходные команды. Для формы, построенной из страницы другого PDF, выбор MediaBox или CropBox определяет её границы. При нестандартном начале координат полезно нормализовать форму переносом, чтобы её левый нижний угол оказался в нуле.
Нельзя создавать некоторые ресурсы, пока активен контекст страницы или формы. Если требуется вложить изображение в форму, безопасная последовательность такова: завершить или приостановить текущий контекст, создать image/form XObject, затем вернуться к контексту и вызвать doXObject. Такое разделение соответствует внутреннему порядку записи PDF-объектов и предотвращает ситуацию, когда описание ресурса оказывается внутри потока содержимого.
Объединение PDF и выбор диапазонов страниц
appendPDFPagesFromPDF добавляет страницы исходного документа в текущий writer. Без диапазона копируются все страницы. Для выборочного импорта создаётся объект диапазона со specificRanges. Индексы начинаются с нуля, поэтому пара 0, 2 означает первые три страницы. Перед построением диапазона стоит проверить getPagesCount у reader, иначе ошибочный последний индекс может завершить операцию исключением или создать неполный результат.
mergePDFPagesToPage переносит содержимое нескольких страниц на одну целевую страницу. Межстраничный callback позволяет изменить матрицу перед следующим импортом, что используется для раскладки две или четыре страницы на лист. Размер исходной страницы, выбранный page box и масштаб необходимо вычислить отдельно. Если просто объединить потоки без матрицы, все страницы окажутся в одном месте и перекроют друг друга.
createFormXObjectsFromPDF превращает страницы источника в формы. Это более гибко, чем прямое добавление: форму можно поставить несколько раз, обрезать выбранным боксом, масштабировать, повернуть и совместить с новым фоном. Для буклетов, превью и листов миниатюр такой путь удобнее, потому что целевая страница создаётся один раз, а каждая исходная страница становится управляемым графическим ресурсом.
DocumentCopyingContext нужен, когда импорт перемежается собственным содержимым или одновременно используются несколько исходных документов. Контекст умеет добавить одну страницу, создать форму из одной страницы, слить страницу с целевой страницей или формой, получить parser источника и сопоставление идентификаторов скопированных объектов. Несколько контекстов допускают чередование материалов из разных файлов без предварительного объединения во временный документ.

const copyA = writer.createPDFCopyingContext('contract.pdf');
const copyB = writer.createPDFCopyingContext('appendix.pdf');
copyA.appendPDFPageFromPDF(0);
copyB.appendPDFPageFromPDF(0);
copyA.appendPDFPageFromPDF(1);
Копирование низкоуровневых объектов применяется, когда стандартного переноса страниц недостаточно. copyObject выполняет глубокую копию косвенного объекта и связанных объектов. copyDirectObjectWithDeepCopy возвращает список ссылок, которые нужно скопировать позже, не нарушая текущую запись контейнера. replaceSourceObjects позволяет заменить исходные объекты заранее подготовленными, например подменить тяжёлые изображения на уменьшенные. Эти операции требуют понимания ссылочной структуры PDF и обязательной проверки результата валидатором.
Изменение существующего PDF
createWriterToModify открывает документ для инкрементального изменения. В файловом варианте можно указать modifiedFilePath и сохранить исходник без перезаписи. В потоковом варианте исходный поток копируется в целевой, после чего новые объекты и таблица перекрёстных ссылок дописываются в конец. Инкрементальная модель удобна для штампов и аннотаций, но размер файла увеличивается, потому что старые объекты физически не удаляются.
PDFPageModifier предназначен для добавления графики на существующую страницу. Конструктор получает writer, индекс страницы и необязательный флаг изоляции графического состояния. При true новая графика защищается от необычной матрицы, клиппинга или цвета, оставшихся в исходном потоке. Затем вызывают startContext, получают обычный content context, рисуют, завершают контекст и фиксируют страницу writePage.
Типичный сценарий штампа включает чтение количества страниц, выбор нужной страницы, создание модификатора, загрузку шрифта и размещение текста или изображения. Для одинакового штампа на каждой странице цикл должен создавать новый PDFPageModifier для каждого индекса. Использовать один модификатор после writePage нельзя: страница уже финализирована. Ресурсы, которые можно переиспользовать, например шрифт или форма печати, создают вне цикла.
Изменение старого содержимого сложнее наложения нового. Для замены слов, удаления оператора или редактирования словаря нужно разобрать исходные объекты, создать изменённую версию с тем же идентификатором через startModifiedIndirectObject и завершить её endIndirectObject. deleteObject отмечает объект удалённым в новой таблице ссылок. Любая ссылка на удалённый объект должна быть пересмотрена, иначе документ останется формально читаемым, но отдельная страница или ресурс будет повреждён.

Цифровая подпись и инкрементальное изменение требуют осторожности. Добавление новой ревизии не переписывает ранее подписанные байты, но просмотрщик может показать, что после подписи документ изменён. Допустимость такого изменения зависит от разрешений подписи и сценария документооборота. Нельзя считать инкрементальную запись способом незаметно изменить подписанный документ; приложение должно явно проверять политики подписи и сохранять исходную ревизию.
Чтение и разбор структуры PDF
createReader возвращает PDFReader. На верхнем уровне доступны версия PDF, количество страниц, trailer, идентификаторы объектов страниц, общее число объектов, состояние шифрования и сведения xref. parsePage даёт объект страницы с MediaBox, CropBox, TrimBox, BleedBox и ArtBox, а parsePageDictionary открывает словарь страницы. Эти методы удобны для предварительной проверки перед копированием, расчёта раскладки и поиска ресурсов.
Парсер предоставляет низкоуровневые типы PDF: словари, массивы, имена, строки, числа, булевы значения, ссылки и потоки. Чтобы добраться до нужного элемента, код проверяет тип объекта, раскрывает косвенную ссылку и читает ключ словаря. Нельзя предполагать, что ресурс находится непосредственно в словаре страницы: многие свойства наследуются от узлов дерева Pages, а контент может быть одним потоком или массивом потоков.
Универсальной команды получить весь текст с координатами в базовом API нет. Текст извлекается интерпретацией операторов содержимого, таблиц кодирования шрифтов, ToUnicode CMap и матриц. В репозитории примеров есть отдельный сценарий извлечения, но он не превращает парсер в готовый движок полнотекстового поиска для любого PDF. Сложные документы с нестандартными шрифтами, вертикальным письмом или текстом в формах требуют дополнительной логики.
То же относится к изображениям. Низкоуровневый парсер позволяет найти XObject, прочитать словарь и декодировать поток, но готовой команды экспорта всех картинок с правильными масками и цветовыми пространствами нет. Изображение может использовать Indexed, DeviceN или ICCBased, иметь отдельную маску, быть частью Form XObject или повторяться через ресурс родительского уровня. Для надёжного извлечения нужен рекурсивный обход и поддержка фильтров.

Зашифрованный файл определяется методом isEncrypted. Документация предупреждает, что парсер не предназначен для полноценной работы с зашифрованным содержимым. Поэтому перед разбором следует остановиться с понятной ошибкой, запросить незашифрованный исходник или использовать другой инструмент, который умеет корректно авторизоваться. Попытка продолжить низкоуровневый обход может дать пустые объекты или исключения, не объясняющие пользователю настоящую причину.
Потоки, буферы и HTTP-ответы
HummusJS использует собственный контракт потоков, отличающийся от стандартного Node.js Stream. Минимальный поток записи реализует write и getCurrentPosition. write получает массив байтов и должен надёжно передать его в целевое хранилище, а getCurrentPosition возвращает число уже записанных байтов. Если позиция отстаёт или перескакивает, смещения объектов и xref становятся неверными, и просмотрщик объявит PDF повреждённым.
Для чтения нужен позиционируемый поток. Методы read, notEnded, setPosition, setPositionFromEnd, skip и getCurrentPosition позволяют библиотеке повторно обращаться к заголовкам, таблицам и потокам. Обычный сетевой поток без возможности seek не подходит напрямую. Его сначала буферизуют, сохраняют во временный файл или оборачивают в объект, который предоставляет произвольный доступ к уже загруженным данным.
Пакет содержит адаптеры для файла, Buffer и HTTP response. PDFStreamForResponse позволяет писать PDF непосредственно в ответ сервера, но заголовки HTTP нужно отправить до первых байтов. Обычно задают Content-Type application/pdf и Content-Disposition inline или attachment. После начала записи нельзя вернуть JSON с описанием ошибки, поэтому все входные данные, права доступа, шрифты и шаблоны желательно проверить заранее.
Генерация в память полезна для вложения PDF в письмо или загрузки в объектное хранилище. Однако большой документ удваивает потребление памяти: одновременно существуют структуры writer и итоговый Buffer. Потоковая запись снижает пик памяти, но усложняет повторную отправку при ошибке. Для пакетной обработки выбирают стратегию по максимальному размеру документа и ограничивают параллелизм, а не запускают десятки тяжёлых writer одновременно.

Сжатие, версии PDF и параметры writer
В параметрах createWriter можно выбрать уровень PDF и включить или отключить сжатие потоков. Сжатие обычно уменьшает размер содержимого страниц и встроенных данных. Отключение полезно при отладке, когда нужно открыть файл в текстовом редакторе и увидеть операторы, но такой документ заметно больше. Изображения JPEG уже сжаты своим кодеком, поэтому отключение Flate не превращает их в несжатые пиксели.
Уровень PDF влияет на доступность отдельных конструкций и совместимость просмотрщиков. Выбирать его только ради меньшего номера не следует: используемые функции должны быть допустимы для указанного уровня. После генерации стоит проверить заголовок, открытие в целевых программах и результат валидатора. Если документ предназначен для архивного стандарта или специализированного печатного процесса, одного параметра версии недостаточно — нужны профили цвета, метаданные, шрифты и дополнительные ограничения.
Опции защиты задают пароль пользователя, пароль владельца и флаги разрешений. Разные просмотрщики могут трактовать ограничения копирования и печати неодинаково, поскольку часть ограничений зависит от соблюдения политики самим приложением. Парольное шифрование нельзя путать с цифровой подписью: первое ограничивает доступ к содержимому, второе подтверждает целостность и автора. Для юридически значимых процессов требуется отдельная инфраструктура подписи.
Файл должен проходить проверку не только на открытие. Практический тест включает количество страниц, размеры боксов, наличие текста и изображений, корректность русских глифов, кликабельность ссылок при их создании, отсутствие обрезанных объектов и приемлемый размер. Для документов, отправляемых клиентам, полезно рендерить контрольные страницы в изображения и сравнивать их с эталоном, чтобы заметить изменение шрифта или координат после обновления окружения.
Установка и нативная часть
Пакет подключается в проект как зависимость npm с именем hummus. Установочный сценарий использует node-pre-gyp: сначала пытается получить готовый бинарный модуль для сочетания ABI Node.js, операционной системы, архитектуры и реализации libc, а при отсутствии совпадения переходит к сборке из исходников. Поэтому успешная установка зависит не только от JavaScript-кода, но и от совместимости нативного двоичного файла.
Если готового бинарника нет, сборка требует компилятора C/C++, Python и node-gyp с системными заголовками. На Windows важны компоненты Visual Studio Build Tools, на Linux — toolchain и стандартные библиотеки разработки, на macOS — Command Line Tools. Точное сообщение об ошибке нужно читать выше финальной строки npm ERR: обычно там указана причина загрузки бинарника, несовпадение ABI или ошибка компилятора.
Контейнеры на musl, прежде всего облегчённые Alpine-образы, могут не принять бинарник, собранный для glibc. Тогда node-pre-gyp запускает исходную сборку, которая тоже может потребовать дополнительные пакеты. Если размер образа не критичен, базовый образ на Debian или Ubuntu часто упрощает установку. Если Alpine обязателен, toolchain добавляют в отдельный build stage, а итоговый модуль проверяют именно в целевом runtime.
После смены основной версии Node.js ABI нативного модуля меняется. Копировать каталог node_modules между образами или машинами нельзя: бинарник, собранный под одну версию Node, архитектуру или libc, может не загрузиться в другой. Зависимости устанавливают внутри целевого build-окружения, а затем выполняют короткий smoke test: require пакета, создание одностраничного PDF, повторное чтение этого PDF и проверка количества страниц.

node -e "const h=require('hummus'); const w=h.createWriter('smoke.pdf'); const p=w.createPage(0,0,100,100); w.writePage(p); w.end(); console.log(h.createReader('smoke.pdf').getPagesCount())
Smoke test отличает проблему загрузки модуля от ошибки конкретного шаблона. Если require завершается сообщением о невозможности открыть binding, проверяют ABI, архитектуру и наличие зависимых динамических библиотек. Если простой PDF создаётся, а производственный падает, причину ищут в шрифте, изображении, входном PDF или последовательности вызовов. Такой разделённый подход сокращает диагностику и не смешивает установку с логикой документа.
Практический сценарий: счёт или акт
Для счёта заранее описывают сетку: поля страницы, ширины колонок, координаты реквизитов, высоту строки и область итогов. Шапку и постоянные реквизиты удобно оформить формой. Переменные данные записываются поверх неё. Таблица строится сверху вниз: текущая координата Y уменьшается после каждой строки, а перед записью проверяется, хватает ли места до нижнего колонтитула. Если места нет, текущая страница завершается, создаётся следующая и повторяется шапка таблицы.
Суммы выравнивают по правой границе измерением ширины текста. Числа форматируют до передачи writer, включая разделители тысяч, десятичную часть и валюту. Нельзя рассчитывать на автоматическую локализацию внутри PDF API. Длинное наименование товара переносится в несколько строк, а высота всей строки таблицы берётся по максимальному числу строк среди колонок, иначе граница пересечёт текст.
Логотип создаётся как reusable XObject один раз. Печать или подпись с прозрачностью берётся из PNG и помещается в заранее определённый прямоугольник. Для факсимиле нужно контролировать правовые правила использования, а не только техническое наложение. Сформированный документ проверяется на отсутствие обрезанных реквизитов, корректный ИНН, номера и даты, а также на совпадение итогов с исходными данными.
Если счёт должен возвращаться из HTTP endpoint, бизнес-валидация выполняется до создания writer. После проверки сервер ставит заголовки и запускает запись в response-адаптер. При пакетной рассылке лучше сначала сформировать файл или Buffer, проверить его размер и только затем прикреплять к письму. Это позволяет повторить отправку без повторной генерации и сохранить точный экземпляр, который получил адресат.
Практический сценарий: штамп, водяной знак и нумерация
Для штампа на готовом документе reader определяет количество страниц, затем createWriterToModify открывает исходник с отдельным путём результата. В цикле для каждой страницы создаётся PDFPageModifier с изоляцией графического состояния. Шрифт и форма штампа готовятся один раз. Координаты рассчитываются по CropBox конкретной страницы, потому что документы в одном файле могут иметь разные размеры и ориентацию.
Полупрозрачность требует работы с графическим состоянием и ExtGState, поскольку простой цвет не содержит альфа-канал. Если прозрачность не реализована, безопаснее использовать светлый цвет и тонкий шрифт, чем подменять её растровой картинкой низкого качества. Водяной знак обычно поворачивают матрицей вокруг центра страницы; перенос к центру, поворот и обратное смещение должны быть рассчитаны в одном блоке q/Q.
Нумерация страниц требует решить, что считать первой страницей и как отображать общее число. reader.getPagesCount даёт физическое количество, а бизнес-нумерация может пропускать обложку или начинаться с другого значения. Строка Страница 2 из 7 измеряется и размещается по правому краю. Для документов с разными CropBox координаты колонтитула вычисляются отдельно для каждой страницы.
После модификации PDF полезно повторно открыть reader и проверить количество страниц. Затем рендерят первую, среднюю и последнюю страницу, чтобы убедиться, что штамп не оказался вне видимой области. Для автоматической проверки можно анализировать размер выходного файла и наличие новой ревизии, но визуальный контроль всё равно нужен для нестандартных исходников с вращением, клиппингом или необычным началом координат.
Практический сценарий: сборка приложения из нескольких документов
Сборка договора и приложений начинается с описания порядка источников и диапазонов. Для простого случая appendPDFPagesFromPDF последовательно добавляет каждый файл. Если из приложения нужны отдельные страницы, диапазоны формируются после проверки числа страниц. Пустой диапазон, повторяющиеся страницы и обратный порядок должны обрабатываться на уровне входной модели, а не оставляться на усмотрение низкоуровневого API.
Когда между импортированными страницами нужна автоматически созданная разделительная страница, используется DocumentCopyingContext. Writer создаёт разделитель с названием приложения, затем контекст добавляет выбранные страницы источника, после чего код переключается на следующий контекст. Такой подход исключает временные промежуточные PDF и позволяет внедрить единый колонтитул или нумерацию в процессе сборки.
Для листа миниатюр страницы источника превращают в Form XObject. Размер выбранного page box переводится в коэффициент масштаба, затем форма размещается в ячейке сетки. Под каждой миниатюрой добавляется номер или имя файла. Важно сохранить пропорции и учесть поворот исходной страницы; иначе альбомный лист будет сжат в портретную ячейку или выйдет за рамку.
Закладки, интерактивные формы, аннотации и вложения могут зависеть от объектов каталога и страниц. Простое копирование страниц не всегда переносит все связи так, как ожидает пользователь. Если эти элементы критичны, нужно проверить конкретный тип документа и при необходимости копировать дополнительные объекты через copying context. Финальный тест должен включать навигацию по закладкам, работу полей и открытие вложений, а не только визуальное совпадение страниц.
Практический сценарий: анализ входного файла
Перед обработкой reader может отклонить неподходящий документ: зашифрованный, пустой, с неожиданным количеством страниц или нестандартным размером. Проверка MediaBox и CropBox помогает обнаружить страницы с огромными координатами, которые приведут к чрезмерному масштабированию. PDF level и xref сведения полезны для диагностики, но не заменяют полноценную валидацию структуры.
Для поиска поля AcroForm код начинает с trailer, переходит к Root и словарю каталога, затем читает AcroForm и массив Fields. Поля могут быть иерархическими, а значение и имя — наследоваться. Видимое представление находится в appearance stream и может не обновиться от простой записи V. Поэтому заполнение формы часто требует изменить значение, сформировать appearance и при необходимости установить флаг обновления внешнего вида.
Для чтения текста пример из отдельного репозитория разбирает content streams и отслеживает текстовые операторы. Такой код следует воспринимать как основу, а не как универсальный OCR. Сканированный PDF содержит изображение без текстовых операторов; HummusJS не распознаёт символы на нём. Для такого файла сначала нужен OCR-движок, после чего распознанный текст можно поместить в PDF отдельным слоем.
Низкоуровневый анализ следует ограничивать ресурсами и глубиной рекурсии. Повреждённый или специально сформированный файл может содержать циклические ссылки, огромные потоки и длинные цепочки объектов. Перед распаковкой данных устанавливают лимит размера, а при обходе сохраняют множество уже посещённых object ID. Обработка недоверенных PDF в отдельном процессе снижает последствия аварии нативного кода.
Ошибки генерации и способы диагностики
Файл не открывается или считается повреждённым
Сначала проверяют, был ли вызван writer.end и завершилась ли запись без исключения. Затем сравнивают фактический размер файла с ожидаемым и смотрят последние байты на наличие финального маркера. Для пользовательского потока перепроверяют getCurrentPosition и отсутствие асинхронной задержки внутри write: контракт ожидает, что переданные байты записаны в правильном порядке. Если поток отправляет данные позднее без координации, xref будет содержать неверные смещения.
При модификации важно не читать и не писать один и тот же файл конкурирующими операциями. Для безопасного сценария задают отдельный modifiedFilePath, закрывают writer и только потом заменяют исходник атомарным переименованием. Если процесс прервётся, исходный документ останется цел. Временные файлы удаляются после успешной проверки, а не сразу после вызова end.
Текст отображается квадратами или пропадает
Проверяют наличие глифов в шрифте, путь к файлу и права чтения. Затем создают минимальный документ только с проблемной строкой. Если латиница видна, а кириллица нет, почти наверняка выбрана гарнитура без кириллического набора или неверная таблица шрифта. Если не виден весь текст, проверяют размер, цвет, координаты и матрицу: строка может быть белой, слишком маленькой или находиться за пределами CropBox.
Изображение повернуто, растянуто или обрезано
При drawImage проверяют transformation, proportional и fit. Для страницы PDF дополнительно проверяют выбранный индекс и page box. Если используется XObject, убеждаются, что q/Q ограничивают матрицу и что box формы покрывает содержимое. Размеры источника получают getImageDimensions и выводят в журнал вместе с вычисленным масштабом, чтобы увидеть ошибку ещё до визуального просмотра.
Установка падает на node-gyp
В журнале находят первую ошибку после попытки получить бинарник. Код 404 у бинарного хоста означает, что для ABI или платформы нет готового файла; далее важен результат fallback-сборки. Сообщения о Python, make, MSBuild или заголовках указывают на toolchain. Сообщение о неверном ELF, архитектуре или символе Node/V8 указывает на бинарник от другого окружения. Удаление node_modules без изменения окружения редко решает причину.
Процесс Node.js аварийно завершается на конкретном PDF
Нативный модуль может завершить процесс там, где обычная JavaScript-библиотека вернула бы исключение. Проблемный файл изолируют, сокращают до минимального набора страниц и обрабатывают в отдельном worker-процессе. Основной сервис задаёт тайм-аут, контролирует код завершения и сохраняет диагностический идентификатор. Недоверенные документы нельзя разбирать в том же процессе, который обслуживает критичные запросы.
Производительность и управление ресурсами
HummusJS полезен там, где нужно писать PDF последовательно и не держать полную модель документа в JavaScript. Однако итоговая производительность зависит от изображений, шрифтов, количества копируемых объектов и скорости целевого потока. Измерять следует не только время writer.end, а весь цикл: чтение входных файлов, подготовку данных, генерацию, запись, повторную проверку и загрузку результата.
Шрифты и повторяемые изображения не нужно создавать заново для каждой строки или страницы. Объект шрифта кэшируют в рамках writer, а постоянную графику превращают в XObject. Внешний глобальный кэш объектов между разными writer использовать нельзя без подтверждённой поддержки: идентификаторы ресурсов принадлежат конкретному документу. Кэшировать можно пути, исходные байты и метрики, но не object ID из уже закрытого PDF.
Большие изображения заранее уменьшают до разумного разрешения. Масштабирование матрицей меняет только видимый размер, но не количество пикселей и не вес встроенного JPEG. Если фотография 8000×6000 отображается в блоке 300×225 пунктов, документ будет неоправданно тяжёлым. Предобработка должна сохранять достаточное разрешение для печати и не применять повторное JPEG-сжатие с низким качеством.
Параллелизм ограничивают по памяти и CPU. Нативная обработка изображений и сборка PDF могут конкурировать за процессор, а несколько больших Buffer — за память. Очередь с фиксированным числом работников даёт более предсказуемую задержку, чем запуск генерации для каждого запроса без лимита. Метрики должны включать размер входа, число страниц, время и причину отказа, чтобы выбирать лимит по реальной нагрузке.
Безопасность обработки PDF
Пути к шрифтам, изображениям и исходным PDF нельзя строить прямой конкатенацией пользовательского ввода. Имена нормализуют, разрешённый каталог фиксируют, а итоговый путь проверяют на принадлежность этому каталогу. Для загружаемых файлов используют случайные внутренние имена и отдельно хранят исходное отображаемое имя. Это предотвращает чтение произвольного файла через последовательности перехода к родительскому каталогу.
Размер и тип входа проверяют до передачи нативному коду. Расширение не подтверждает формат, поэтому читают сигнатуру и устанавливают предел размера. Для изображений ограничивают пиксельные размеры, чтобы небольшая сжатая картинка не развернулась в гигантский буфер. Для PDF ограничивают страницы, размер потоков и время обработки. Отказ должен быть контролируемым и не раскрывать внутренние пути сервера.
Генерация из пользовательского текста не равна выполнению JavaScript, но данные всё равно могут нарушить макет или создать огромный файл. Ограничивают длину полей, число строк и количество элементов массива. Управляющие символы фильтруют или отображают явно. Для ссылок и действий PDF применяют белый список схем и не добавляют произвольные JavaScript actions в документ.
Нативную зависимость проверяют сканером компонентов и фиксируют точную версию в lock-файле. Бинарники и исходная сборка должны поступать из контролируемых каналов. В CI полезно сохранять хэш установленного пакета и результат smoke test. Контейнер запускают без лишних прав, с read-only файловой системой там, где это возможно, и отдельным временным каталогом с квотой.
Тестирование шаблонов и регрессий
Модульный тест проверяет вычисления координат, перенос строк, форматирование сумм и выбор диапазонов без создания PDF. Интеграционный тест генерирует документ, открывает его PDFReader и проверяет количество страниц, размеры и ключевые объекты. Визуальный тест рендерит страницы и сравнивает изображения с допуском, поскольку небольшие различия сглаживания не должны считаться ошибкой.
Эталонные документы должны покрывать кириллицу, длинные слова, пустые значения, максимальное число строк, многостраничную таблицу, прозрачный PNG, многостраничный TIFF и импорт PDF с нестандартным CropBox. Отдельно проверяются страницы в портретной и альбомной ориентации. Один простой пример не обнаружит ошибки, которые проявляются только после переноса на вторую страницу или смены шрифта.
Для модификации нужен набор входных PDF из разных источников. Документы, созданные офисным пакетом, сканером, браузером и профессиональной издательской системой, имеют разную структуру. Тест проверяет наложение на страницу с массивом content streams, наследуемыми ресурсами и поворотом. Если приложение принимает чужие документы, тестовый набор должен отражать это разнообразие.
После изменения окружения Node.js или базового контейнера запускают установочный smoke test и полный набор PDF-тестов. Успешный require недостаточен: бинарник может загрузиться, но иначе работать с изображениями или шрифтами из-за системных библиотек. Артефакты теста сохраняют, чтобы сравнить размер, хэш и визуальный результат с предыдущей сборкой.
Сравнение HummusJS с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| HummusJS | Потокового создания, низкоуровневого изменения и копирования объектов PDF из Node.js | Нативный модуль и низкоуровневый API требуют подготовленного окружения |
| MuhammaraJS | Проектов, которым нужен совместимый преемник API HummusJS и поддержка современных сред Node.js | Сохраняет зависимость от нативной сборки и особенностей PDF-структуры |
| pdf-lib | Кроссплатформенной работы с PDF на JavaScript без нативного аддона, включая браузерные сценарии | Большие документы часто обрабатываются как массивы байтов в памяти |
| PDFKit | Последовательной генерации новых отчётов, счетов и графических документов в Node.js | Не предназначен для полноценного редактирования существующего PDF |
| Hummus Recipe | Более высокоуровневых операций рисования, текста и модификации поверх движка Hummus | Абстракция закрывает не все низкоуровневые сценарии и наследует ограничения основы |
| PDF Commander | Ручного редактирования страниц, текста и объектов пользователем без программирования | Не заменяет серверный API для автоматической пакетной генерации |
HummusJS выбирают, когда нужен прямой контроль над PDF-объектами, потоками, инкрементальной модификацией и копированием страниц внутри Node.js-процесса. MuhammaraJS рациональнее для нового кода, которому нужна близкая модель API в более современной поддерживаемой реализации. pdf-lib удобнее, когда критична работа без нативного аддона, а PDFKit — когда задача ограничена созданием новых документов. Hummus Recipe сокращает объём низкоуровневых команд, а PDF Commander лучше подходит сотруднику, который должен вручную исправить конкретный файл, а не строить автоматизированный конвейер.
Когда HummusJS оправдан в проекте
Библиотека оправдана, если команда уже понимает структуру PDF и использует проверенные шаблоны: генерирует много однотипных документов, добавляет машинные штампы, собирает приложения, переносит страницы как формы или анализирует словари и потоки. В таких задачах низкоуровневый контроль уменьшает зависимость от визуального редактора и позволяет связать документ с данными сервиса без промежуточного ручного этапа.
Для нового проекта важно заранее подтвердить установку в целевом окружении, собрать контрольный PDF и проверить требуемые операции на реальных входных документах. Если задача состоит только в создании простого отчёта, более высокий уровень API может снизить стоимость сопровождения. Если требуется браузерная работа без сервера, нативный модуль не подходит по модели выполнения. Если нужны сложное извлечение текста, OCR или полноценный визуальный редактор, эти компоненты выбирают отдельно.
Надёжная интеграция отделяет расчёт макета от вызовов writer, хранит шрифты и ресурсы вместе с приложением, ограничивает входные файлы, выполняет генерацию в изолированном worker и проверяет результат reader или внешним валидатором. Документ становится воспроизводимым артефактом: одинаковые данные и ресурсы дают одинаковую структуру и расположение элементов, а ошибки фиксируются на конкретном этапе.
Итоговый критерий выбора — не количество методов, а соответствие рабочему процессу. HummusJS даёт инструменты создания страниц, текста, изображений, векторной графики, форм, копирования и низкоуровневого разбора. Пользователь отвечает за макет, перенос строк, совместимость нативной сборки, безопасность входных PDF и проверку результата. При таком разделении ответственности библиотека остаётся эффективным инструментом для автоматизированных PDF-конвейеров.
Контрольный список внедрения
Окружение
Зафиксировать версию Node.js и архитектуру, установить пакет в чистом образе, выполнить require и smoke test создания/чтения одной страницы. Не переносить node_modules между системами.
Для пункта Окружение полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Ресурсы
Проверить лицензии и наличие кириллических глифов в шрифтах, размеры изображений, прозрачность PNG и доступность всех файлов по стабильным путям.
Для пункта Ресурсы полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Макет
Описать MediaBox, поля, координатную систему, перенос строк, разбиение таблиц на страницы, повтор шапки и правила выравнивания чисел.
Для пункта Макет полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Входные PDF
Ограничить размер и страницы, проверить сигнатуру и шифрование, запускать недоверенный разбор в отдельном процессе с тайм-аутом.
Для пункта Входные PDF полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Модификация
Писать в отдельный modifiedFilePath, использовать PDFPageModifier на каждом индексе, изолировать графическое состояние и повторно открывать результат reader.
Для пункта Модификация полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Потоки
Убедиться, что write сохраняет порядок байтов, getCurrentPosition точен, а read-поток поддерживает позиционирование. Заголовки HTTP задавать до записи.
Для пункта Потоки полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Проверка
Сверять количество страниц и боксы, рендерить контрольные страницы, тестировать несколько просмотрщиков и сохранять регрессионные эталоны.
Для пункта Проверка полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Наблюдаемость
Записывать время генерации, размер входа и выхода, число страниц, код завершения worker и категорию ошибки без утечки пользовательских данных.
Для пункта Наблюдаемость полезен автоматический тест, который выполняется в CI и в целевом контейнере. Проверка должна давать однозначный результат и сохранять диагностический файл только при отказе. Так изменение зависимости, системной библиотеки или шаблона обнаруживается до выдачи документа пользователю.
Координаты и единицы измерения
PDF использует точки: 72 единицы соответствуют одному дюйму. Размеры, полученные из макета в миллиметрах, переводят по формуле mm × 72 / 25,4 и округляют только на границе вывода. Раннее округление на каждой колонке накапливает ошибку, поэтому последняя граница таблицы может сместиться. Удобно хранить функции mm(), pt() и topY(), а бизнес-модель макета задавать в привычных единицах. Координата Y в PDF растёт снизу вверх, поэтому положение от верхнего края вычисляется как высота страницы минус верхнее поле и высота объекта. Для вложенной области полезно вводить локальное начало координат через cm и возвращать состояние оператором Q.
Проверочный шаблон с линейкой по краям страницы быстро выявляет ошибку масштаба, перепутанный MediaBox и неверный поворот. В тестах сравнивают не только конечные координаты, но и сумму ширин колонок с рабочей шириной страницы.
Подготовка шрифтов
До генерации шрифт проверяют на наличие всех символов, которые реально придут из данных: кириллицы, неразрывного пробела, знака рубля, длинного тире, математических знаков и нужных кавычек. Наличие файла TTF ещё не гарантирует требуемые глифы. Функция измерения текста должна использовать тот же размер и тот же объект шрифта, что и функция вывода; иначе перенос строк и правое выравнивание расходятся с фактическим результатом. Жирное начертание подключают отдельным файлом, а не имитируют многократным наложением текста. Для резервного шрифта заранее задают правило выбора на уровне строки или отдельных фрагментов.
Если в документе появляются пустые квадраты, сначала извлекают кодовые точки проблемной строки и сверяют карту символов шрифта. Замена кодировки входной строки не исправит отсутствие глифа. Контрольный PDF должен содержать весь алфавит и специальные символы проекта.
Разметка многострочного текста
writeText выводит строку в заданной позиции, но не превращает произвольный абзац в готовый текстовый блок. Перенос строят отдельно: нормализуют пробелы, разделяют слова, измеряют кандидатную строку и переносят слово, если ширина превышает границу. Слишком длинное слово обрабатывают по символам или разрешённым точкам переноса, иначе оно выйдет за колонку. Высоту блока рассчитывают как число строк, умноженное на межстрочный интервал, с учётом дополнительного отступа после абзаца. Перед выводом блока проверяют остаток страницы и решают, можно ли разделить абзац или нужно перенести его целиком.
В таблице высота строки равна максимальной высоте содержимого среди всех ячеек. Сначала рассчитывают перенос во всех колонках, затем рисуют фон и границы, после чего выводят строки текста. Такой порядок исключает пересечение текста линиями.
Ресурсы и повторное использование
Одинаковый логотип, фоновую форму или печать не следует декодировать и встраивать заново на каждой странице. Ресурс создают один раз как Form XObject или image XObject, сохраняют идентификатор и многократно размещают с нужной матрицей. Это уменьшает размер файла и ускоряет обработку. Объект шрифта также получают один раз на документ и передают функциям макета. Кэш не должен переживать writer, потому что идентификаторы объектов относятся к конкретному PDF и недействительны в другом документе. Для разных размеров одного изображения достаточно менять матрицу размещения, а не создавать несколько копий данных.
При неожиданном росте файла сравнивают число страниц, число повторяющихся ресурсов и размер потоков. Если одинаковая картинка встроена десятки раз, ищут место, где функция создания ресурса вызывается внутри страничного цикла.
Поворот страницы и page boxes
Ориентацию нельзя определять только сравнением ширины и высоты MediaBox. Страница может иметь запись Rotate, а видимая область задаваться CropBox. Для штампа или номера сначала читают нужный box, затем учитывают угол поворота 0, 90, 180 или 270 градусов и преобразуют координаты в видимую систему. На странице с Rotate 90 нижний правый угол пользователя не совпадает с нижним правым углом исходного потока. Надёжная функция размещения принимает box и rotation и возвращает матрицу, а не набор частных поправок в каждом сценарии.
Тестовый набор включает четыре угла поворота, разные MediaBox и CropBox, а также отрицательное начало координат. Штамп должен оставаться на одинаковом визуальном расстоянии от края во всех вариантах.
Предварительная проверка изображений
Перед drawImage проверяют формат, пиксельные размеры, число кадров, ориентацию и наличие альфа-канала. Огромная фотография, которая выводится маленькой, всё равно требует декодирования и может раздувать память; её разумно уменьшить до требуемого разрешения заранее. Для печати выбирают плотность, соответствующую физическому размеру на странице, а для экранного счёта не сохраняют десятки мегапикселей. JPEG используют для фотографий, PNG — для схем, текста и прозрачности. Многостраничный TIFF требует явного index, иначе в документ попадёт не тот кадр или только первый.
Автоматический preflight отклоняет повреждённый файл до открытия writer и сообщает понятную причину. Отдельно проверяют цветовой режим и прозрачность, потому что необычные TIFF-палитры и маски чаще вызывают различия между просмотрщиками.
Копирование страниц и зависимых объектов
При импорте страницы важен не только её content stream. Вид зависит от словаря Resources, шрифтов, изображений, форм, графических состояний и унаследованных записей дерева Pages. DocumentCopyingContext переносит связанные объекты и сопоставляет старые ссылки новым, поэтому ручное копирование одного словаря страницы обычно недостаточно. Если одну исходную страницу размещают несколько раз как форму, форму создают один раз и повторяют, а не импортируют исходный файл заново. Диапазоны проверяют относительно нулевой индексации до начала операции.
После сборки сравнивают визуальный результат страниц с исходниками и отдельно тестируют аннотации, поля и закладки, если они нужны. Такие элементы могут находиться вне обычного потока страницы и требуют дополнительной логики.
Обработка ошибок и завершение writer
Операции с writer оборачивают так, чтобы исключение не оставляло пользователю неполный файл с правильным именем. Результат записывают во временный путь, вызывают end, повторно открывают reader и только после проверки атомарно переименовывают. Ошибки делят на входные данные, ресурс шаблона, нативную загрузку, разбор PDF, запись потока и внутреннее ограничение макета. В журнале фиксируют этап, страницу, идентификатор шаблона и безопасные размеры, но не содержимое персональных документов. Один общий текст не удалось создать PDF лишает поддержку возможности найти причину.
Если исключение возникло посередине HTTP-ответа, код статуса уже может быть отправлен. Поэтому критичные проверки выполняют до первой записи, а для сложных документов сначала формируют временный файл или Buffer.
Параллельная генерация
Несколько запросов не следует направлять в один writer или один пользовательский stream. Каждый документ получает собственные объекты и путь. Параллелизм ограничивают по памяти и времени CPU, поскольку декодирование изображений, обработка шрифтов и нативная запись выполняются внутри процесса. Для тяжёлых заданий используют очередь worker-процессов: сбой или превышение памяти завершается только в одном worker, а веб-процесс остаётся доступным. Число worker выбирают по измерениям на максимальном шаблоне, а не по числу логических ядер.
Метрики включают время ожидания очереди, время генерации, максимальный размер входа, страницы и размер результата. Рост одного показателя помогает отличить перегрузку от конкретного повреждённого документа.
Воспроизводимость результата
Для регрессионных тестов нужно учитывать, что метаданные, даты и порядок объектов могут менять хэш при одинаковом визуальном содержимом. Если требуется побитовое сравнение, все переменные поля задают явно и контролируют порядок обхода коллекций. Чаще полезнее структурное сравнение: число страниц, размеры, выбранные словари, извлечённые контрольные строки и рендер страниц. Растровое сравнение выполняют с небольшим допуском, поскольку разные версии рендерера могут по-разному сглаживать края шрифта.
Эталон обновляют только после просмотра различий. Автоматическое принятие нового изображения при каждом сбое превращает визуальный тест в формальность и скрывает смещение макета.
Типизированная оболочка проекта
При большом количестве шаблонов вызовы HummusJS удобно закрыть внутренним адаптером. Он принимает типизированные команды: текстовый блок, изображение, линия, таблица, импорт страниц и штамп. Адаптер валидирует координаты, существование ресурсов и обязательные поля, а затем переводит команды в низкоуровневые методы content context. Бизнес-код не должен самостоятельно вызывать q, cm и Q в десятках мест. Это снижает риск утечки графического состояния и позволяет заменить реализацию отдельной операции без переписывания расчёта документа.
Типы особенно полезны для параметров page box, индексов страниц и единиц измерения. Отдельные типы для миллиметров, точек и нулевого индекса предотвращают ошибки, которые JavaScript обнаружит только в готовом PDF.
Диагностика content stream
Когда объект не виден, временно отключают сжатие writer и открывают поток страницы в текстовом виде. Проверяют наличие BT/ET для текста, q/Q вокруг преобразований, матрицу cm, оператор Do для формы или изображения и команды построения пути. Затем сравнивают словарь Resources с именами, использованными в потоке. Невидимый объект часто находится за пределами CropBox, имеет нулевой масштаб, полностью перекрыт клиппингом или рисуется белым цветом. Если после пользовательских операторов забыто восстановление Q, ошибка распространяется на все следующие элементы.
Минимальный воспроизводимый файл должен содержать одну страницу и один проблемный ресурс. Удаление таблиц и бизнес-данных помогает понять, связана ли ошибка с PDF-командой или с расчётом макета.
Развёртывание в serverless-среде
В функции по требованию нативный binding должен быть собран для точной операционной системы, архитектуры и ABI среды выполнения. Пакет, установленный на рабочем ноутбуке, нельзя просто архивировать для другого runtime. Сборку выполняют в совместимом контейнере или официальном build-окружении платформы. Также учитывают размер deployment package, доступный временный диск, лимит памяти и максимальное время запроса. Шрифты и шаблоны включают в артефакт и обращаются к ним через абсолютный путь внутри функции.
Холодный запуск проверяют отдельно от тёплого. Если загрузка binding и шрифтов занимает заметное время, worker можно прогревать коротким созданием PDF, но результат прогрева не должен смешиваться с пользовательским потоком.
Приём недоверенных PDF
Файл от пользователя рассматривают как недоверенный бинарный ввод. До createReader ограничивают размер, число файлов и время загрузки, проверяют сигнатуру %PDF и сохраняют под случайным именем вне публичного каталога. Сам разбор запускают в worker с тайм-аутом и лимитом памяти. Заявленное расширение .pdf не является проверкой. Шифрованные, повреждённые или чрезмерно сложные документы отклоняют с отдельным кодом, не пытаясь бесконечно повторять операцию.
После обработки временные файлы удаляют независимо от результата. В журнал не помещают полный путь клиента, текст документа и пароли. Для расследования достаточно технического идентификатора задания и хэша входа, если политика хранения это допускает.