Назад в Центр знаний
Начало работы

Puppeteer PDF: поля, шрифты и разрывы страниц

Создавайте PDF через Puppeteer с заданными параметрами, CSS печати, управляемой пагинацией и проверкой отсутствующих китайских знаков.

BotBrowser Team

Документация

Нужна структурированная документация по теме Начало работы?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

Puppeteer может сохранить страницу в PDF, но результат подчиняется правилам печатной компоновки, а не просто превращает длинный скриншот в файл. Профиль BotBrowser может задавать определённый набор шрифтов для повторяемого рендеринга браузера; он не выбирает CSS печати, не ждёт данные приложения и не гарантирует, что установленные файлы шрифтов содержат все нужные глифы CJK. Явно задайте формат бумаги и поля, дождитесь сигнала готовности самой страницы и загрузки шрифтов, затем проверьте разрывы страниц и несколько характерных знаков, прежде чем считать файл завершённым.

Кратко

  • page.pdf() использует CSS-медиа print. Задайте размер бумаги, поля и правила разрывов в @media print и @page.
  • Явно задавайте format, margin и printBackground, если от них зависит результат; включайте displayHeaderFooter, только если передаёте шаблоны.
  • Дождитесь сигнала готовности данных приложения и document.fonts.ready. Завершённая загрузка шрифтов не доказывает, что выбранный шрифт содержит все китайские, японские или корейские глифы.
  • Для новых правил используйте break-before, break-after и break-inside; имена page-break-* остаются устаревшими псевдонимами в CSS печати.
  • Пустой квадрат вместо глифа требует проверки установленных шрифтов и самого PDF, а не предположения, что Chromium не встроил шрифт.

Содержание

Схема создания PDF в Puppeteer: параметры page.pdf, CSS печати, разрывы страниц и проверка глифов CJK.

Что рендерит page.pdf()

page.pdf() печатает текущую страницу с CSS-типом медиа print. Поэтому правило внутри @media print может изменить PDF, не меняя экранное представление. page.screenshot() создаёт растровое изображение окна или страницы; PDF разбивает содержимое на страницы с размерами бумаги, печатными полями и правилами фрагментации. Параметры из руководства по качеству скриншотов решают другую задачу: сохранить пиксели конкретного вида, а не распределить содержимое по бумажным листам.

Эта разница объясняет многие сообщения вроде «PDF проигнорировал мой CSS». Возможно, таблица стилей содержит только экранные правила либо CSS задаёт размер страницы через @page, а вызов PDF одновременно указывает format бумаги. В документации Puppeteer параметр preferCSSPageSize задаёт приоритет размера из CSS; иначе параметры PDF могут иметь преимущество, а содержимое может масштабироваться под лист. Выберите один источник истины для размеров бумаги, вместо того чтобы менять оба, пока результат не покажется приемлемым.

API возвращает PDF-буфер для текущего состояния страницы. Она не ожидает асинхронный контракт данных приложения, если вызывающий код не сделал этого заранее. networkidle0 подходит некоторым страницам, но не является универсальным признаком «отчёт готов»: аналитика, периодический опрос или долгие запросы могут удерживать соединение после того, как сам отчёт уже готов. Селектор или событие приложения, например data-report-ready="true", задаёт условие завершения, связанное с данными отчёта.

Задать бумагу, поля и печатные цвета

Чаще всего пригодность файла определяют несколько явных параметров:

ПараметрЧто задаётПрактический выбор
formatИменованный формат бумаги, например A4 или LetterУкажите его, когда формат зафиксирован контрактом вывода.
marginВерхнее, правое, нижнее и левое поляЗадайте единицы mm или in; согласуйте их с @page.
printBackgroundВключение фоновой графикиВключите, если цветные блоки или фоновые изображения несут смысл.
displayHeaderFooterПечать шаблонов колонтитуловВключите вместе с шаблонами для номеров страниц и метаданных.
preferCSSPageSizeПриоритет CSS-размера @pageИспользуйте, если размеры бумаги задаёт таблица стилей.

Задокументированные значения по умолчанию не являются рекомендациями по дизайну: формат бумаги по умолчанию - Letter, поля не заданы, фон не печатается, колонтитулы выключены. Для базовой печати это может быть приемлемо, но станет неожиданностью, если конвейер отчётов предполагает A4, цветные диаграммы или номера страниц. Сохраняйте выбранные параметры в конфигурации теста или сервиса, чтобы обновление не меняло незаметно смысл слова «PDF» для последующих систем.

import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });

try {
  const page = await browser.newPage();
  await page.goto('https://example.test/report/42', { waitUntil: 'domcontentloaded' });
  await page.waitForSelector('[data-report-ready="true"]');
  await page.evaluate(async () => {
    await document.fonts.ready;
    return document.fonts.status;
  });

  const pdf = await page.pdf({
    format: 'A4',
    margin: { top: '18mm', right: '14mm', bottom: '18mm', left: '14mm' },
    printBackground: true,
    displayHeaderFooter: true,
    headerTemplate: '<span></span>',
    footerTemplate: '<span class="pageNumber"></span>/<span class="totalPages"></span>',
  });

  await fs.writeFile('report.pdf', pdf);
} finally {
  await browser.close();
}

Замените пример URL и селектор готовности на маршрут и сигнал своего приложения. В этой версии размер бумаги задаёт API; если им управляет таблица стилей, задайте @page { size: A4; } и укажите preferCSSPageSize: true. Не допускайте, чтобы два уровня молча расходились. Шаблоны колонтитулов - небольшие фрагменты HTML; обычная таблица стилей страницы не заменяет стили внутри этих фрагментов, поэтому для контроля вида используйте встроенные стили.

Поля можно задать либо в параметрах page.pdf(), либо в @page. Не добавляйте оба набора без необходимости: полезная область страницы зависит от взаимодействия размера бумаги, полей и содержимого, а конкурирующие значения усложняют разбор обрезанного текста. Если отчётом управляет @page, проверьте размеры в файле и включите preferCSSPageSize: true; если контракт приложения задаёт format и margin через API, оставьте таблице печати только стили содержимого и разрывов.

Выбор бумаги также влияет на поведение существующих CSS-размеров. format выбирает стандартный лист; width и height - более низкоуровневая альтернатива для нестандартного размера. В документации параметров PDF сказано, что при наличии format он имеет приоритет. Поэтому передача всех трёх параметров не делает нестандартный формат «ещё более явным». Выберите именованный формат либо пару ширины и высоты, а ориентацию задайте соответствующим форматом или правилом CSS-страницы. Один раз проверьте реальные размеры PDF; не выводите их из окна браузера, это отдельный входной параметр.

Для отчёта из нескольких частей параметр pageRanges позволяет сократить большой документ до полезной выборки: например, обложки, плотной таблицы и страницы с длинным текстом CJK. Это быстрее, чем открывать все страницы после изменения CSS. Но диапазон страниц не выбирает содержимое: номера могут сдвинуться, если в предыдущем разделе появилась строка. Оставьте отдельную проверку общего числа страниц и точечный рендер страниц повышенного риска, чтобы удачная выборка не скрыла отсутствие последнего раздела.

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

Колонтитулы - отдельный небольшой документ. При displayHeaderFooter: true задайте headerTemplate и footerTemplate; документация описывает подстановки date, title, url, pageNumber и totalPages. Номер страницы полезен в PDF, который будут печатать или цитировать; логотип на всю ширину может занять печатную область и перенести содержимое на дополнительный лист. Сделайте шаблоны компактными, задайте шрифт и интервалы непосредственно в них, проверьте первую, среднюю и последнюю страницы: длинный заголовок может поместиться на одной и обрезаться на другой. Для одностраничной квитанции пустой шаблон может быть понятнее включённых колонтитулов без видимого содержимого.

Управлять пагинацией через CSS печати

Таблица стилей печати должна определять, где допустимы разрывы, какие элементы следует держать вместе и что показывать только на бумаге. @page настраивает геометрию листа, а @media print ограничивает стили режимом печати:

@page {
  size: A4;
  margin: 18mm 14mm;
}

@media print {
  .chapter:not(:first-child) {
    break-before: page;
  }

  .result-card,
  figure,
  table tr {
    break-inside: avoid;
  }

  .screen-only {
    display: none;
  }
}

Используйте break-before: page, чтобы начать раздел с нового листа. Назначайте break-inside: avoid небольшому объекту, например карточке, рисунку или строке таблицы, который станет нечитаемым при разделении. Это не обещает уместить на одном листе элемент любой высоты: если он выше доступного пространства, движку всё равно придётся разместить его содержимое. Не назначайте правило удержания всей длинной секции отчёта: это может оставить большой пустой участок на предыдущей странице или вызвать неудобное переполнение.

В старых таблицах стилей встречаются page-break-before, page-break-after и page-break-inside. Эти устаревшие свойства являются совместимыми псевдонимами соответствующих современных свойств break-*; в новых правилах применяйте современные имена. Правило page-break-before: always не заменяет проверку фактической границы: на место разрыва влияют поля, содержимое перед заголовком и высота элемента.

Если кнопка «следующая страница» в PDF ничего не меняет, проверьте путь рендеринга по порядку:

  1. Убедитесь, что вызов PDF использует медиарежим печати, а правило разрыва находится в таблице стилей, загруженной этим маршрутом.
  2. Проверьте, не конфликтует ли @page с API-параметрами format или margin; оставьте единственный управляющий уровень.
  3. Изучите вычисленные стили печати и размеры элемента. Селектор только для экрана или незагруженная таблица стилей могут сделать правило разрыва неприменимым.
  4. Найдите высокий родительский блок с break-inside: avoid; перенесите правило на более мелкую часть, которую действительно нужно сохранить целиком.
  5. Снова отрендерите затронутый диапазон и проверьте заголовок, таблицу и нижний колонтитул около границы.

Для длинной таблицы повтор заголовка и запрет разрыва строки решают разные задачи. Элемент thead можно оформить в режиме печати как группу заголовка таблицы, а строки снабдить break-inside: avoid; ни один параметр не гарантирует, что вся таблица поместится на одном листе. В контролируемых отчётах добавьте многостраничный пример с короткой строкой, высокой строкой и заголовком раздела, чтобы изменение CSS не прошло проверку на одно-страничной заготовке.

Фрагментация может выявить компоновку, которая не проверяется на экране. Строка flex или grid, помещавшаяся в широкое окно, может стать выше при печати на ширину бумаги; панель с фиксированной высотой может обрезать текст после переноса печатным шрифтом; заголовок у нижнего края страницы может остаться без первого абзаца. Правило печати break-after: avoid-page может удержать заголовок вместе со следующим блоком, а экранные кнопки, навигацию и закреплённые элементы можно скрыть. Проверяйте вычисленные стили в режиме печати на той же версии браузера, которая создаёт PDF: экранная каскадность не показывает, какое правило сработало при печати.

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

Диагностика глифов CJK и проверка файла

Квадрат вместо китайского, японского или корейского знака обычно означает, что выбранный путь шрифта не предоставил этот глиф. Причиной может быть отсутствующее семейство шрифтов, неполное покрытие, незагруженный веб-шрифт или другой резервный шрифт на хосте. Одного значения document.fonts.status === "loaded" недостаточно: оно описывает набор загрузки, а не подтверждает, что конкретный файл шрифта содержит все символы документа.

Начните с небольшой тестовой строки: упрощённый и традиционный китайский, японская кана, корейский хангыль, пунктуация и символы из реального документа. Дождитесь явного сигнала готовности страницы и document.fonts.ready, затем проверьте набор шрифтов хоста и PDF. В Linux-команде fc-match "Noto Sans CJK SC" покажет установленный шрифт, который Fontconfig сопоставит с этим семейством; fc-list :lang=zh перечислит установленные шрифты, заявляющие поддержку китайского. Эти команды проверяют инвентарь хоста, а не фактический шрифт для каждого глифа; это диагностический шаг, не доказательство содержимого PDF.

Затем отрендерите PDF-страницу с тестовой строкой в изображение и осмотрите полученный файл. pdffonts report.pdf может показать, какие ресурсы шрифтов использует PDF и встроены ли они; pdftoppm -f 1 -singlefile -png report.pdf /tmp/report-page создаёт изображение страницы для проверки. Шрифт может присутствовать в списке ресурсов PDF и при этом оказаться неправильной подстановкой для знака; сравнивайте сами глифы, а не только метаданные. Для среды, где нужен предсказуемый CJK-текст, зафиксируйте сборку браузера и предоставьте одинаковый набор шрифтов каждому исполнителю. В руководстве по загрузке шрифтов и стабильности компоновки объясняется, как запоздавший шрифт перемещает текст; генерация PDF добавляет режим печати и пагинацию к той же задаче выбора шрифта.

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

Извлечение текста - полезная дополнительная проверка. pdftotext report.pdf - покажет, содержит ли результат ожидаемые символы и порядок чтения, но не доказывает, что просмотрщик отрисует их нужной формой. PDF может содержать извлекаемый Unicode и показывать резервный квадрат либо выглядеть корректно, но иметь неудобный порядок извлечения для вспомогательных средств. Рассматривайте визуальный осмотр, извлечение текста, число страниц и ресурсы шрифтов как отдельные сигналы: каждый отвечает на свой вопрос о файле.

Если файл отличается только переносами строк или числом страниц, сначала сравните его с минимальным двухстраничным примером, а уже потом меняйте поля. Меняйте одну переменную за раз: сначала формат бумаги, затем файлы шрифтов и сигнал готовности, затем фрагментацию. Перенос длинного абзаца через границу часто приемлем; заголовок без первого абзаца или разорванная строка таблицы требуют точечного CSS печати. Общий break-inside: avoid для всех контейнеров небезопасен, поскольку блок выше листа нельзя сохранить целиком.

Частые вопросы о PDF

Почему @media print меняет PDF, но не скриншот?

page.pdf() по умолчанию рендерит в режиме печати. Скриншот обычно использует экранный режим, если тест явно не поменял его. Если продукт требует оба результата, проверяйте экранный и печатный варианты отдельно.

Почему пропали фоновые цвета?

У API-параметра printBackground значение по умолчанию - false. Включите его, если фон несёт информацию, а затем проверьте PDF: просмотрщики и принтеры могут дополнительно управлять цветом.

Почему китайский символ превращается в квадрат только в CI?

На исполнителе может отсутствовать то же семейство шрифтов или покрытие глифов, что на машине разработки; веб-шрифт также может не успеть загрузиться до создания PDF. Сравните шрифты исполнителя, дождитесь готовности набора шрифтов документа и отрендерите страницу PDF. Не выводите причину только из версии браузера.

Использовать page-break-before или break-before?

В новых CSS-правилах используйте break-before. page-break-before остаётся устаревшим псевдонимом, но переименование свойства не исправит конфликт размера бумаги, отсутствие таблицы печати или слишком высокий родительский элемент.

Источники

Практический вывод

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

#Puppeteer PDF#CSS Печати#Разрывы Страниц#Шрифты#Автоматизация

Эта статья входит в руководство: Начало работы с автоматизацией браузера: карта

Выполните ту же проверку в BotBrowser

Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.