Кейс · Frontend Engineering · PDF pipeline

Почему PDF ломался ровно на 7 слайдах из 18 — и не чинился

Презентацию агентства «Медиацентр» правили несколько раз подряд: номер слайда «наезжал» на контент на слайдах 6, 8–13, а после починки та же проблема возвращалась при следующем экспорте. Разбор показал: дело не в вёрстке конкретных слайдов, а в одном непросуженном media-запросе, который делал экспорт непредсказуемым. Ниже — как это нашли и закрыли на уровне архитектуры.

Роль: диагностика и правка вёрстки
Стек: HTML / CSS / Headless Chrome
Проверено: 18 / 18 слайдов
Багов устранено: 2 независимых
01 · Задача

Симптом был на поверхности. Причина — нет

Презентацию редактировали и переэкспортировали много раз. Каждый раз номер страницы «съезжал» на одних и тех же слайдах — 6, 8, 9, 10, 11, 12, 13 — а вручную подвинутый текст снова расходился при следующей правке контента.

Слайд 13: номер страницы наезжает на блок «Бюджет: 450 000 ₽»
Баг №1

Нумерация наезжает на контент

Плотные grid-сетки (4 колонки метрик, 7 колонок процесса) на этих слайдах схлопывались в 2 колонки, контент вырастал по высоте и упирался в подпись «13 — 18» внизу страницы.

слайды 6, 8, 9, 10, 11, 12, 13
Слайд 1: слово «системе» обрезано в PDF-просмотрщике macOS
Баг №2

Градиентный текст обрезан

Найден отдельно, по скриншотам с Mac: заголовок с градиентной заливкой текста рендерился нормально в браузере, но в PDF-просмотрщике macOS обрывался цветным прямоугольником на середине слова.

слайды 1, 3, 5, 11–13, 16
02 · Диагностика

Искали не «где криво», а «что общего»

Точечная правка каждого слайда ничего не давала — проблема возвращалась после следующего изменения текста. Значит, источник был не в контенте, а в общем правиле вёрстки.

Изучил структуру проекта

Один index.html на 18 слайдов, общий styles.css, три альтернативных сценария экспорта в PDF: headless Chrome, Playwright (export.py), Puppeteer (export_node.js). Общий стиль — источник истины для всех способов.

Сопоставил список «сломанных» слайдов с версткой

Все проблемные слайды — те, где вёрстка держится на плотных сетках: .process на 7 колонок, .case-metrics на 4 колонки, .facts на 2×3. Совпадение не случайное — это единственное, что их объединяет.

Нашёл общий знаменатель в media-запросах

Мобильные брейкпоинты @media (max-width:1024px) и (max-width:680px) не были ограничены типом устройства. Они одинаково активны и в узком окне браузера, и при экспорте в PDF — как только «бумага» уже 1024 CSS-пикселей.

Было — сработает при любой ширине, включая печать
@media (max-width:1024px){ .stats{grid-template-columns:repeat(2,1fr)} .process{grid-template-columns:repeat(2,1fr)} /* …и так на каждой плотной сетке */ }

Подтвердил гипотезу экспериментом

Экспорт через диалог печати браузера (⌘P) даёт предпросмотр уже узкого «листа». Сетки схлопываются в 2 колонки → контент вырастает по высоте → упирается в колонтитул. Поэтому проблема была не «случайной», а воспроизводилась каждый раз именно на одних и тех же слайдах.

03 · Второй, независимый баг

Один и тот же почерк — разный механизм

Скриншоты с Mac показали второй дефект, не связанный с первым: градиентная заливка текста (background-clip:text) корректно отображалась в браузере, но обрывалась в macOS Preview.

Chrome печатает градиент как маску поверх текста
Внутри PDF это не текст с цветом, а изображение-маска — приём, который сам по себе валиден.
Quartz-рендерер Preview обрабатывает такую маску иначе
Буквы обрезаются, а на их месте — сплошной цветной прямоугольник. В других PDF-читалках дефект не проявлялся, поэтому раньше его не заметили.
Заодно нашлась третья, менее заметная деталь
box-shadow карточек Preview рисовал серыми плашками поверх светлых слайдов.
ПУТЬ РЕНДЕРИНГА HTML + CSS-градиент Chrome print → PDF PDF-файл (маска) открыть на Mac Chrome / другой читалка → ок macOS Preview буква обрезана Разные движки рендеринга PDF по-разному интерпретируют одну и ту же маску-градиент. Вывод: не полагаться на технику, которая ведёт себя по-разному в разных читалках PDF.
04 · Решение

Правка архитектуры, а не отдельных слайдов

Каждое изменение — общее правило в styles.css, а не координаты конкретного текста. Это гарантирует, что баг не вернётся после следующей правки контента.

Мобильные брейкпоинты ограничены экраном
@media screen and (max-width:…) — при печати/экспорте эти правила больше не могут сработать ни при какой ширине «бумаги».
Номер страницы — в фиксированных пикселях
position:absolute;left:80px;bottom:34px вместо величин в vw, зависевших от ширины окна.
Под нумерацию зарезервирована полоса
96px нижнего padding на слайде — контент физически не может дойти до колонтитула, сколько бы текста в нём ни было.
Переполнение уходит вверх, а не вниз
margin:auto вместо центрирования по flex — если контент когда-нибудь станет выше страницы, он прижмётся к верху, а не наедет на футер.
Десктопные сетки задублированы в блоке печати
Страховка независимо от того, каким инструментом в итоге экспортируют PDF.
Градиент в печати — сплошной фирменный цвет
На экране градиент остаётся; в PDF используется техника, которую одинаково понимают все читалки.
Один авторитетный сценарий экспорта
export.sh на headless Chrome вместо диалога печати браузера — тот самый источник нестабильной ширины «бумаги».
05 · Верификация

Не «выглядит нормально», а измерено

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

Координаты текста, а не пиксели скриншота
Из PDF извлечены буквальные координаты номера страницы на всех 18 страницах и сверены как множество — должна остаться ровно одна Y-координата.
Рендер тем же движком, что у Preview
Каждая страница отрисована через CoreGraphics (sips) — движок, которым сама macOS показывает PDF, а не приближение через сторонний рендерер.
$ python3 verify_marks.py

страниц: 18
размеры страниц: {(960.0, 540.0)}
Y-координата номера на всех страницах: {29.17}

✓ OK — единственное значение на 18 из 18
06 · Результат

Что изменилось для агентства

18/18
слайдов с идентичной нумерацией
2
независимых бага устранены в архитектуре
1
команда для воспроизводимого экспорта
0
точечных правок отдельных слайдов
07 · Стек и структура

Из чего собран проект

Презентация — статичные HTML/CSS/JS без сборки и фреймворков: 18 секций на одной странице, скролл-снап, IntersectionObserver для анимаций появления. Экспорт в PDF — headless-браузер, печатная медиа-таблица управляет вёрсткой страницы.

HTML5CSS Grid / FlexboxVanilla JS Headless ChromePlaywrightPuppeteer pypdfium2CoreGraphics / sips
├─ presentation/ │ ├─ index.html │ ├─ styles.css — печатный блок переписан │ ├─ script.js │ ├─ export.sh — единая точка экспорта │ ├─ export.py │ ├─ export_node.js │ ├─ assets/, fonts/ │ └─ Медиацентр_презентация+кейсы.pdf ├─ case-media/ — скриншоты до/после ├─ index.html — эта страница └─ README.md
Медиацентр

Реклама у блогеров, превращённая в систему

Подбор под KPI, проверка, согласование, контроль выхода и прозрачная отчётность — агентство инфлюенс-маркетинга, для которого готовилась эта презентация.