FPDF позволяет формировать PDF из PHP-кода: добавлять страницы заданного формата, выводить текст и таблицы, размещать JPEG, PNG, GIF и WebP, рисовать линии и прямоугольники, создавать ссылки, колонтитулы и метаданные, а затем сохранять документ, отправлять его в браузер или получать как строку. Основные инструменты — AddPage(), SetFont(), Cell(), MultiCell(), Write(), Image(), методы позиционирования и Output(); поэтому макет собирается последовательно, с точным контролем координат, отступов, цветов и переходов на новую страницу.
Рабочий интерфейс FPDF состоит из методов класса и текущего состояния документа. Сценарий задаёт формат страницы, единицы измерения и поля, открывает страницу, выбирает шрифт, перемещает курсор и выводит элементы в нужном порядке. Результат сразу воспроизводим: одинаковые входные данные и одинаковые координаты дают одинаковый макет, что удобно для счетов, актов, справок, сертификатов, каталогов, этикеток и отчётов, формируемых по шаблону.
Перед началом полезно разложить будущую страницу на зоны: шапку, основной поток, таблицу, примечания и подвал. Затем для каждой зоны выбирают подходящий инструмент: Cell() для короткой строки или ячейки, MultiCell() для абзаца с переносами, Write() для текучего текста, Image() для растровой графики, Line() и Rect() для рамок. Такой порядок снижает число ручных поправок и помогает заранее решить, где разрешён автоматический разрыв страницы, а где высоту блока нужно вычислять самостоятельно.
Скачать FPDF
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет визуального редактора
- Нет встроенного UTF-8
- Нет импорта готовых PDF
Как устроен рабочий процесс в FPDF
Документ создаётся как объект класса, который хранит параметры страницы, выбранные цвета, шрифт, координаты курсора и уже подготовленные ресурсы. Конструктор принимает ориентацию, единицу измерения и формат. Для ориентации используются портретный и альбомный режимы; в качестве единиц доступны пункты, миллиметры, сантиметры и дюймы. Размер можно выбрать из A3, A4, A5, Letter и Legal либо передать парой ширины и высоты. Это позволяет одним и тем же набором методов готовить обычный отчёт A4, узкую квитанцию, карточку товара или лист этикеток.
После создания объекта вызывают AddPage(). Метод не просто добавляет пустой лист: он завершает подвал предыдущей страницы, переносит текущую позицию к верхнему и левому полям, запускает Header() и восстанавливает активные шрифт, цвета и толщину линии. Поэтому при переходе на следующий лист не требуется повторять SetFont(), SetTextColor() и SetLineWidth(), если оформление остаётся прежним. Если странице нужна другая ориентация, размер или поворот, параметры передают непосредственно в AddPage(); угол поворота должен быть кратен 90 градусам.
Текст нельзя выводить до первого выбора шрифта. SetFont() задаёт семейство, начертание и размер в пунктах, после чего Cell(), MultiCell(), Write() и Text() используют эти параметры. В конце Output() закрывает незавершённый документ и направляет результат в выбранное место. Благодаря этой последовательности минимальный сценарий легко читать сверху вниз: подключение класса, создание объекта, добавление страницы, выбор шрифта, вывод содержимого и выдача файла.
<?php
require __DIR__ . '/fpdf/fpdf.php';
$pdf = new FPDF('P', 'mm', 'A4');
$pdf->AddPage();
$pdf->SetFont('Helvetica', '', 12);
$pdf->Cell(0, 8, 'PDF created by FPDF', 0, 1);
$pdf->Output('F', __DIR__ . '/output/document.pdf');
В рабочем проекте до создания файла стоит проверить существование каталога назначения и права на запись. FPDF сообщает об ошибках через свой механизм Error(), поэтому исключение из окружающего кода не всегда заменяет предварительную проверку пути. Для веб-ответа также важно не печатать отладочные строки, пробелы или предупреждения до Output(): любые лишние байты способны повредить заголовки и структуру отдаваемого PDF.
Подключение класса к проекту
При ручном подключении каталог FPDF размещают внутри проекта и загружают fpdf.php через require_once. Путь следует строить от __DIR__, а не от текущего рабочего каталога: один и тот же генератор может запускаться веб-сервером, командой CLI или обработчиком очереди из разных директорий. Рядом должны оставаться папка стандартных шрифтов и другие файлы пакета, на которые рассчитывает класс.
require_once __DIR__ . '/lib/fpdf/fpdf.php';
$pdf = new FPDF();
$pdf->SetMargins(15, 18, 15);
$pdf->SetAutoPageBreak(true, 20);
При управлении зависимостями через Composer приложение загружает vendor/autoload.php, после чего доступен тот же класс FPDF. В коде шаблона не стоит одновременно подключать ручную копию и пакет из vendor: две копии одного класса вызывают ошибку повторного объявления или делают непонятным, какой файл фактически загружен. Выберите один способ и закрепите его в конфигурации проекта.
Генератор удобно вызывать из отдельного сервиса. Контроллер или консольная команда получает данные, проверяет права пользователя, передаёт нормализованную структуру шаблону и решает, куда направить результат. Сам класс разметки не должен читать параметры запроса, выполнять SQL и отправлять заголовки: такое разделение позволяет одинаково использовать его для предварительного просмотра, пакетной задачи и почтового вложения.
final class DocumentService
{
public function buildReport(array $data): string
{
$pdf = new ReportPdf('P', 'mm', 'A4');
$pdf->render($data);
return $pdf->Output('S');
}
}
Статические ресурсы также разрешают через абсолютные пути. Если шаблон получает относительное имя логотипа, сервис сопоставляет его с разрешённым файлом и передаёт полный путь. Это устраняет зависимость от настроек include_path и защищает от подстановки произвольного файла. Для пользовательских изображений добавляют проверку MIME-типа, размеров и пиксельных ограничений до запуска Image().
После подключения полезен короткий диагностический тест: сформировать одну страницу, сохранить её в временный каталог и проверить ненулевой размер. Затем тестируют выдачу строкой и HTTP-ответ отдельно. Такой порядок отличает проблему загрузки класса или прав на файл от проблемы заголовков веб-приложения.
Страница, единицы измерения и система координат
Начало координат находится в левом верхнем углу, координата X растёт вправо, а Y — вниз. Это отличается от математических графиков, где вертикальная ось обычно направлена вверх, зато соответствует привычной разметке экранной страницы. При единицах миллиметры вызов SetXY(20, 35) устанавливает курсор на 20 мм от левого края и 35 мм от верхнего. Размер шрифта при этом всегда задаётся в типографских пунктах, независимо от выбранной единицы документа.
Поля задаются SetMargins(), либо отдельно SetLeftMargin(), SetTopMargin() и SetRightMargin(). По умолчанию каждое основное поле составляет около сантиметра. Текущую позицию возвращают GetX() и GetY(); SetX(), SetY() и SetXY() меняют её. Отрицательное значение X или Y отсчитывается от правого либо нижнего края, поэтому SetY(-20) удобно использовать перед выводом подвала. У SetY() есть дополнительный эффект: по умолчанию он возвращает X к левому полю, что полезно для новой строки, но может удивить при точной раскладке двух элементов.
GetPageWidth() и GetPageHeight() дают фактические размеры текущего листа. Это надёжнее, чем жёстко записывать 210 и 297: альбомный лист, пользовательский формат или смешанный документ изменят доступную область. Центральное размещение можно рассчитать как разность ширины страницы и ширины объекта, делённую пополам. Правое выравнивание строится аналогично, но дополнительно вычитается правое поле.
$pageWidth = $pdf->GetPageWidth();
$left = 15;
$right = 15;
$contentWidth = $pageWidth - $left - $right;
$pdf->SetMargins($left, 18, $right);
$pdf->SetXY($left, 30);
$pdf->Cell($contentWidth, 9, 'Monthly report', 1, 1, 'C');
Для сложной страницы полезно хранить размеры в именованных переменных: ширину контента, высоту строки, промежуток между колонками, высоту шапки. Тогда изменение формата не превращается в поиск десятков чисел по коду. Координаты следует округлять разумно: дробные миллиметры допустимы, но чрезмерная точность затрудняет чтение и почти не видна при печати.
Пользовательский формат и смешанные страницы
Пара ширины и высоты позволяет создать нестандартный лист, например 100 × 150 мм. Тот же документ может содержать страницы разных размеров: параметры передаются в очередной AddPage(). Практический пример — отчёт с портретными текстовыми страницами и альбомной широкой таблицей. Перед возвратом к портретной ориентации нужно снова явно указать нужные параметры, иначе новый лист наследует значение, заданное конструктором или вызовом страницы в соответствии с переданными аргументами.
Поворот страницы кратно 90 градусам меняет свойство отображения листа, но не заменяет продуманную раскладку. Если содержимое рассчитано под портретную ширину, простое вращение не распределит таблицу по новой области. Для адаптивного шаблона сначала считывают фактическую ширину и высоту, затем вычисляют колонки и только после этого выводят элементы.
Вывод коротких строк с Cell()
Cell() создаёт прямоугольную область от текущей позиции. Ширина 0 растягивает её до правого поля, высота задаёт вертикальный шаг, а параметр border включает полную рамку либо отдельные стороны L, T, R и B. Параметр align поддерживает левое, центральное и правое выравнивание. Фоновая заливка применяется только при fill=true, поэтому одного SetFillColor() недостаточно.
Пятый параметр определяет положение курсора после ячейки. Значение 0 оставляет его справа, 1 переносит к началу следующей строки, 2 ставит под ячейкой. При построении строки таблицы несколько Cell() вызываются с ln=0, а последняя — с ln=1. Если забыть перенос, следующая строка продолжится за правым краем. Если ширина текста превышает ширину ячейки, Cell() не переносит его автоматически; строка может выйти за границы или наложиться на соседний столбец.
$pdf->SetFont('Helvetica', 'B', 10);
$pdf->SetFillColor(235, 235, 235);
$pdf->Cell(95, 8, 'Item', 1, 0, 'L', true);
$pdf->Cell(30, 8, 'Qty', 1, 0, 'C', true);
$pdf->Cell(40, 8, 'Amount', 1, 1, 'R', true);
$pdf->SetFont('Helvetica', '', 10);
$pdf->Cell(95, 8, 'Service package', 1, 0);
$pdf->Cell(30, 8, '2', 1, 0, 'C');
$pdf->Cell(40, 8, '18 500.00', 1, 1, 'R');
Для контроля переполнения применяют GetStringWidth(). Метод возвращает ширину строки в текущем шрифте и размере. Если значение больше доступной ширины за вычетом внутренних отступов, можно уменьшить размер шрифта, обрезать текст по принятому правилу либо перейти к MultiCell(). Автоматическое уменьшение до нечитаемого размера лучше не использовать: таблица станет формально аккуратной, но потеряет практическую ценность.
Внутренний горизонтальный отступ ячейки определяется состоянием класса и обычно невелик. Когда нужно строго выровнять числа по правому краю, оставляют небольшой запас в ширине столбца и используют align='R'. Для денежных сумм форматирование выполняют до передачи в FPDF: библиотека выводит строку и не рассчитывает валюту, разделители тысяч или округление.
Абзацы, переносы и текучий текст
MultiCell() предназначен для блока с автоматическими и явными переносами. Он создаёт столько строк-ячеек, сколько требуется, размещая их одну под другой. Ширина 0 означает пространство до правого поля. Доступны левое, центральное, правое выравнивание и выключка по ширине. Рамка и заливка применяются ко всему последовательному блоку, а высота параметра задаёт шаг каждой строки, а не общую высоту абзаца.
После MultiCell() курсор оказывается у левого поля под блоком. Это критично для таблицы с многострочными ячейками: если сразу вывести соседнюю ячейку, она начнётся на следующей строке. Типовой приём — сохранить X и Y, вывести MultiCell(), запомнить конечный Y, затем вернуть курсор к сохранённой позиции и нарисовать соседние ячейки. После завершения строки курсор переводят на максимальный Y среди всех столбцов.
Write() ведёт себя как поток: начинает с текущей позиции, переносит строку у правого поля или по символу новой строки и оставляет курсор сразу после последнего знака. Этот метод удобен для абзаца с фрагментами разных начертаний или для ссылки внутри текста. Можно вызвать Write() несколько раз, меняя SetFont() и SetTextColor(), и получить единую строку без ручного расчёта X для каждого фрагмента.
Text() отличается тем, что печатает строку в заданной точке по базовой линии и не двигает текущий курсор. Его используют для подписей на диаграмме, отметок в фиксированном бланке и элементов, положение которых не должно влиять на последующий поток. Для обычного абзаца Text() неудобен: переносы и высоту строк пришлось бы считать самостоятельно.
$pdf->SetFont('Helvetica', '', 10);
$pdf->MultiCell(0, 5.5,
$paragraph,
0,
'J'
);
$pdf->Ln(2);
$pdf->Write(6, 'See section ');
$pdf->SetFont('', 'U');
$pdf->Write(6, 'Payment terms', $termsLink);
$pdf->SetFont('', '');
Высоту абзаца полезно оценивать до вывода, если за ним должен следовать неделимый блок. В базовом API нет метода, который возвращает число строк для произвольного MultiCell(). Обычно создают собственную функцию расчёта на основе ширины символов либо используют пробный вывод в расширенном классе. Для простого документа достаточно проверять остаток страницы по GetY() и добавлять новый лист с запасом.
Шрифты, начертания и кодировки
SetFont() обязательно вызывается хотя бы один раз до печати текста. Встроенные семейства — Courier, Helvetica или его синоним Arial, Times, Symbol и ZapfDingbats. Для обычных семейств доступны сочетания жирного, курсивного и подчёркнутого начертаний. Размер указывается в пунктах; вызов SetFontSize() меняет только кегль, сохраняя семейство и стиль.
Стандартные шрифты используют однобайтовую кодировку cp1252, ориентированную на западноевропейские символы. Поэтому строка UTF-8 с кириллицей не станет читаемой только от выбора Helvetica. Симптом — пустые места, вопросительные знаки или набор псевдографики. Для русского текста нужен подходящий TrueType, OpenType с TrueType-контурами либо Type1-шрифт и определение, подготовленное MakeFont(). Текст перед выводом должен соответствовать кодировке, для которой создана таблица символов.
AddFont() регистрирует семейство в документе. Сначала MakeFont() анализирует файл шрифта и создаёт файл определения; при встраивании рядом должен быть доступен сам шрифт или подготовленный сжатый ресурс. Поиск идёт в каталоге, переданном четвёртым параметром, затем в FPDF_FONTPATH и, наконец, в папке font рядом с fpdf.php. Ошибка Could not include font definition file означает, что определение не найдено по этим путям либо имя не совпало.
// Один раз подготовьте определение шрифта утилитой MakeFont.
$pdf->AddFont('DejaVuSans', '', 'dejavusans.json');
$pdf->AddFont('DejaVuSans', 'B', 'dejavusans-bold.json');
$pdf->SetFont('DejaVuSans', '', 10);
$pdf->MultiCell(0, 6, $textInConfiguredEncoding);
Имя семейства в AddFont() выбирается автором шаблона и затем повторяется в SetFont(). Стиль должен соответствовать зарегистрированному файлу: если добавлено только обычное начертание, запрос жирного не создаёт его автоматически. Для каждого используемого варианта готовят отдельное определение. Подмена стандартного имени допустима, но усложняет поддержку: по коду становится неясно, какой реальный файл встроен.
Встраивание обеспечивает одинаковый вид на компьютерах, где исходного шрифта нет, но увеличивает размер файла. Для отчёта из сотен однотипных страниц выбирают одно семейство с минимальным числом начертаний. Перед внедрением шрифта проверяют его лицензию: техническая возможность добавить файл не означает разрешение распространять его внутри PDF.
Практическая схема для кириллицы
- Выберите шрифт, содержащий русские глифы и разрешающий встраивание.
- Создайте определение MakeFont() с нужной таблицей кодировки.
- Поместите определение и шрифтовый ресурс в предсказуемый каталог проекта.
- Зарегистрируйте обычное и требуемые начертания через AddFont().
- Приведите входные строки к той же кодировке перед Cell(), MultiCell() и Write().
- Проверьте буквы Ё, кавычки, тире, неразрывный пробел и знак валюты на тестовой странице.
Для приложений, где весь поток данных уже UTF-8 и требуется широкий набор письменностей, базовая модель FPDF может оказаться неудобной. Расширения семейства tFPDF либо другая библиотека с нативной обработкой Unicode уменьшают число преобразований. Выбор должен зависеть не от количества строк кода, а от набора языков, правил переноса и требований к шрифтовым подмножествам.
Цвет, линии, рамки и простая графика
FPDF хранит отдельно цвет текста, обводки и заливки. SetTextColor() влияет на последующий текст, SetDrawColor() — на линии и границы, SetFillColor() — на закрашенные области. Один аргумент задаёт оттенок серого, три аргумента — компоненты RGB от 0 до 255. После смены цвета он действует до следующего вызова, в том числе на новой странице, поскольку состояние оформления восстанавливается.
Line() рисует отрезок между двумя точками, Rect() — прямоугольник. Толщину обводки задаёт SetLineWidth() в единицах документа. У прямоугольника можно выбрать только обводку, только заливку или сочетание. Эти примитивы достаточны для разделителей, рамок карточек, шкал, простых диаграмм и фона заголовка, но не заменяют полноценный векторный редактор.
$pdf->SetDrawColor(90, 90, 90);
$pdf->SetFillColor(245, 245, 245);
$pdf->SetLineWidth(0.3);
$pdf->Rect(15, 30, 180, 24, 'DF');
$pdf->SetTextColor(30, 30, 30);
$pdf->SetXY(20, 36);
$pdf->SetFont('Helvetica', 'B', 13);
$pdf->Cell(170, 6, 'Summary', 0, 1);
Цвета следует возвращать к основным значениям после выделенного блока. Иначе следующий абзац унаследует белый текст или цвет заливки, рассчитанный для шапки. Безопасный шаблон — небольшая функция, которая полностью задаёт стиль конкретного компонента: шрифт, текст, обводку, заливку и толщину линии. Тогда компонент меньше зависит от того, что было выведено перед ним.
Для печатных документов нужно учитывать, что методы принимают RGB, а типография может ожидать иной цветовой процесс. Контраст проверяют на обычном чёрно-белом принтере: светло-серая рамка и бледный текст часто исчезают. Тонкие линии меньше примерно четверти миллиметра могут выглядеть неравномерно в зависимости от устройства и масштаба просмотра.
Изображения: форматы, размеры и прозрачность
Image() размещает JPEG, PNG, GIF и WebP. Для GIF и WebP требуется расширение GD. JPEG допускает оттенки серого, полноцветный режим и CMYK; PNG поддерживает серые, индексированные и полноцветные варианты, а прозрачность сохраняется. В анимированном GIF используется только первый кадр, поэтому анимация в итоговом PDF не воспроизводится.
Размер можно задать шириной и высотой в единицах документа. Если указана только одна сторона, вторая вычисляется с сохранением пропорций. Нулевые размеры означают автоматический расчёт, а при отсутствии размеров изображение размещается с плотностью 96 dpi. Отрицательное значение ширины или высоты интерпретируется как требуемое разрешение в dpi; например, -300 удобно для логотипа, который должен печататься в физическом размере, рассчитанном по 300 dpi.
Координаты X и Y определяют левый верхний угол. Если Y не передан, используется текущая вертикальная позиция; при включённом автоматическом разрыве FPDF сначала проверяет, помещается ли изображение, а после вызова переносит текущий Y к его нижней границе. При явно заданных координатах курсор не следует считать автоматически синхронизированным с потоком — следующую позицию лучше установить самостоятельно.
$logo = __DIR__ . '/assets/logo.png';
$pdf->Image($logo, 15, 12, -300);
$photoWidth = 70;
$pdf->Image($photoPath, 15, 45, $photoWidth, 0, 'JPEG');
$pdf->SetY(45 + 52);
Формат обычно определяется по расширению. Для динамического ресурса или имени без расширения тип передают явно. Путь может быть локальным или сетевым, но удалённый ресурс добавляет зависимость от настроек PHP, сертификатов, времени ответа и доступности сервера. В надёжном процессе изображение сначала загружают, проверяют MIME-тип, размер и лимиты, сохраняют во временный контролируемый файл, а затем передают FPDF.
Если одно и то же изображение используется многократно, библиотека встраивает его в PDF один раз. Это полезно для логотипа в Header(): повтор на каждой странице не умножает объём ресурса. Однако два файла с одинаковой картинкой, но разными путями или преобразованиями, могут восприниматься как разные объекты. Для каталога лучше переиспользовать один подготовленный файл.
Почему изображение не открывается
- Расширение файла не соответствует реальному формату; передайте корректный type либо перекодируйте ресурс.
- GIF или WebP обрабатывается без GD; установите расширение или заранее преобразуйте картинку в PNG/JPEG.
- PHP не имеет права читать каталог; проверьте абсолютный путь и разрешения.
- Файл повреждён либо содержит неподдерживаемый вариант; откройте и пересохраните его проверенным редактором.
- Удалённый адрес недоступен из процесса PHP; не связывайте генерацию критичного документа с внешним запросом.
Перед вставкой больших фотографий их уменьшают до разумного пиксельного размера. Сжатие страницы не отменяет веса исходного JPEG или PNG, а чтение многомегапиксельного файла расходует память. Для печати обычно достаточно разрешения, соответствующего физическому размеру на странице; картинка 6000 × 4000 пикселей не даёт преимущества, если выводится шириной 40 мм.
Колонтитулы и номера страниц
Для повторяющейся шапки создают подкласс FPDF и переопределяют Header(). Метод вызывается автоматически при каждой AddPage() после установки позиции к верхнему левому полю. Внутри можно разместить логотип, название документа, дату и разделитель. Footer() вызывается перед завершением страницы и при закрытии документа; обычно он устанавливает Y отрицательным значением от нижнего края, выбирает небольшой шрифт и печатает номер.
class ReportPdf extends FPDF
{
public function Header(): void
{
$this->SetFont('Helvetica', 'B', 11);
$this->Cell(0, 7, 'Operations report', 0, 1, 'R');
$this->Line(15, 22, $this->GetPageWidth() - 15, 22);
$this->Ln(6);
}
public function Footer(): void
{
$this->SetY(-15);
$this->SetFont('Helvetica', '', 8);
$this->Cell(0, 6, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
}
$pdf = new ReportPdf();
$pdf->AliasNbPages();
PageNo() возвращает номер текущей страницы. Общее число заранее неизвестно, поэтому AliasNbPages() регистрирует маркер, по умолчанию {nb}, который заменяется при закрытии документа. Маркер должен быть выведен тем же способом, что и обычный текст. Если документ собирается частями, AliasNbPages() вызывают до формирования страниц.
Шапка и подвал занимают реальное место. Верхнее поле должно оставлять высоту Header(), а нижний предел автоматического разрыва — пространство Footer(). Иначе основной текст наложится на повторяющиеся элементы. Если шапка различается по разделам, подкласс хранит текущий заголовок в свойстве, которое обновляется перед AddPage().
В Header() и Footer() лучше полностью задавать собственный стиль и не рассчитывать на случайное состояние основного текста. После AddPage() FPDF восстанавливает шрифт и цвета, установленные до перехода, поэтому оформление потока продолжится. Однако явные установки внутри колонтитула делают код понятнее и защищают от изменений в соседних компонентах.
Автоматические и управляемые разрывы страниц
Автоматический разрыв включён по умолчанию, а граница срабатывания расположена примерно в двух сантиметрах от нижнего края. SetAutoPageBreak(true, margin) меняет этот резерв; false отключает механизм. Cell(), MultiCell(), Write() и Image() в потоковом режиме проверяют, выходит ли следующий элемент за предел. Если выходит, FPDF завершает текущий лист, добавляет новый и продолжает вывод.
Механизм хорошо работает для последовательных строк одинаковой высоты, но не знает смысловой структуры. Заголовок раздела может остаться последней строкой страницы, а таблица начаться на следующей. Чтобы этого избежать, до неделимого блока сравнивают GetY() плюс ожидаемую высоту с нижней границей. При недостатке места вызывают AddPage() вручную.
function ensureSpace(FPDF $pdf, float $height, float $bottom = 20): void
{
$limit = $pdf->GetPageHeight() - $bottom;
if ($pdf->GetY() + $height > $limit) {
$pdf->AddPage();
}
}
ensureSpace($pdf, 28);
$pdf->SetFont('Helvetica', 'B', 12);
$pdf->Cell(0, 8, 'Payment details', 0, 1);
$pdf->SetFont('Helvetica', '', 10);
$pdf->MultiCell(0, 5.5, $details);
AcceptPageBreak() позволяет изменить решение об автоматическом переносе. В многоколоночной раскладке переопределённый метод сначала переводит курсор в следующую колонку и возвращает false, а после последней колонки сбрасывает номер колонки и возвращает true. Так текст заполняет столбцы сверху вниз без ручного разбиения исходной строки.
В таблице с многострочными ячейками высота строки должна определяться до вывода. Если сначала напечатать левый столбец, а затем выяснить, что правый занял четыре строки и пересёк границу страницы, рамки разойдутся. Корректный алгоритм вычисляет максимальное число строк, проверяет место, при необходимости добавляет страницу и только затем рисует всю строку.
Отключать автоматические разрывы стоит только там, где координаты полностью контролируются: этикетки, билет, фиксированный бланк. При свободном пользовательском тексте выключенный режим приводит к содержимому за пределами листа. После специального блока режим включают снова и восстанавливают нижний резерв.
Многоколоночная раскладка
Колонки строятся управлением левым полем и X. Для трёх колонок вычисляют ширину доступной области, вычитают два промежутка и делят остаток на три. При переходе к следующей колонке SetLeftMargin() устанавливает её начало, SetX() синхронизирует курсор, а SetY() возвращает его к верхней позиции текста. После последней колонки исходное левое поле обязательно восстанавливают.
class ColumnsPdf extends FPDF
{
private int $column = 0;
private float $columnTop = 32;
public function setColumn(int $number): void
{
$this->column = $number;
$x = 15 + $number * 62;
$this->SetLeftMargin($x);
$this->SetX($x);
$this->SetY($this->columnTop);
}
public function AcceptPageBreak(): bool
{
if ($this->column < 2) {
$this->setColumn($this->column + 1);
return false;
}
$this->setColumn(0);
return true;
}
}
Переопределение выше демонстрирует принцип, но в производственном шаблоне X и ширины берут из свойств, а не фиксируют. MultiCell() получает ширину колонки и выводит поток до границы страницы. Изображение, которое должно занимать две колонки, лучше размещать отдельным блоком до или после колонок: автоматический перенос не понимает обтекание сложного объекта.
Колонки требуют контроля высоты шапки. columnTop должен находиться ниже Header(), а нижний резерв — выше Footer(). Если раздел начинается не с новой страницы, верхняя позиция берётся из GetY() в момент начала колонок. После завершения важно определить максимальный Y всех колонок; иначе следующий полноширинный блок может наложиться на длиннейшую колонку.
Для газетной раскладки с изображениями, врезками и балансировкой колонок базовых методов мало. FPDF не перераспределяет строки так, чтобы колонки получились одинаковой высоты. Баланс выполняют заранее: разбивают содержание на части, измеряют число строк или создают собственный механизм пробной раскладки. Для технического отчёта обычно достаточно последовательного заполнения.
Таблицы: от простых строк до многострочных ячеек
Базовая таблица строится последовательностью Cell(). Ширины столбцов хранятся в массиве, заголовок получает отдельный стиль, данные форматируются заранее. Числовые значения выравниваются вправо, короткие коды — по центру, описания — влево. Сумма ширин не должна превышать ширину контента; иначе последняя ячейка выйдет за правое поле.
$widths = [25, 95, 25, 35];
$headers = ['Code', 'Description', 'Qty', 'Total'];
$pdf->SetFont('Helvetica', 'B', 9);
foreach ($headers as $i => $header) {
$pdf->Cell($widths[$i], 8, $header, 1, 0, 'C', true);
}
$pdf->Ln();
$pdf->SetFont('Helvetica', '', 9);
foreach ($rows as $row) {
$pdf->Cell($widths[0], 7, $row['code'], 1);
$pdf->Cell($widths[1], 7, $row['name'], 1);
$pdf->Cell($widths[2], 7, (string)$row['qty'], 1, 0, 'C');
$pdf->Cell($widths[3], 7, $row['total'], 1, 1, 'R');
}
Ширины по содержимому и аккуратная рамка
GetStringWidth() помогает подобрать ширину по самому длинному значению, но полностью автоматический расчёт редко даёт хороший макет: столбец описания должен получать остаток, а идентификатор — ограниченную ширину. Практичнее установить минимумы и максимумы, затем распределить свободное место. Внешнюю рамку и горизонтальные линии можно рисовать отдельно Line() и Rect(), чтобы не удваивать толщину на общих границах.
Зебра создаётся переключением fill для каждой второй строки. Цвет заливки задают один раз, а логическое значение вычисляют по индексу. Выделение не должно ухудшать читаемость на принтере. Заголовок таблицы повторяют после каждого ручного AddPage(); для этого код вывода заголовка выносят в функцию.
Многострочные ячейки
Когда описание переносится, строка перестаёт иметь фиксированную высоту. Сначала рассчитывают число строк для каждого столбца, берут максимум и умножают на высоту строки. Затем проверяют место на странице. Каждую ячейку выводят MultiCell(), возвращая X и Y к началу строки, а рамку рисуют Rect() на общую высоту. После всех столбцов SetXY() перемещает курсор к началу следующей строки.
Функция подсчёта строк должна использовать метрики текущего шрифта и ту же полезную ширину, что MultiCell(). Она проходит по символам, учитывает пробелы и явные переводы строк. Ошибка даже в одну строку даёт разрыв рамки или лишнее пустое пространство. Для длинных непрерывных последовательностей, например токена без пробелов, нужно заранее определить правило: переносить по символам, уменьшать размер или обрезать отображение.
foreach ($rows as $row) {
$height = calculateRowHeight($pdf, $row, $widths, 5);
ensureSpace($pdf, $height);
$x = $pdf->GetX();
$y = $pdf->GetY();
foreach ($row as $i => $value) {
$pdf->Rect($x, $y, $widths[$i], $height);
$pdf->SetXY($x, $y);
$pdf->MultiCell($widths[$i], 5, $value, 0, $align[$i]);
$x += $widths[$i];
}
$pdf->SetXY($pdf->lMargin, $y + $height);
}
В примере обращение к защищённому свойству lMargin допустимо только внутри подкласса; во внешнем коде лучше хранить левое поле отдельно. Это характерная причина ошибок при копировании фрагментов: учебная функция может предполагать наследование, а вставляется в обычный скрипт. Всегда сопоставляйте область видимости с архитектурой проекта.
Итоги, объединённые области и продолжение
В базовом API нет объединения ячеек как в электронной таблице. Эффект создают ячейкой, ширина которой равна сумме нескольких столбцов, и соответствующим изменением порядка вывода. Вертикальное объединение требует ручной рамки и контроля строк. Итоговую строку удобно отделить верхней линией, выделить жирным и не разрешать ей отрываться от последней строки данных.
Если таблица продолжается на новом листе, шапка должна повторяться, а состояние зебры — сохраняться или осознанно сбрасываться. Для аудиторского документа полезно добавлять номер страницы и идентификатор отчёта в Footer(), чтобы распечатанные листы не потеряли контекст.
Ссылки внутри документа и переходы
AddLink() создаёт идентификатор внутренней цели. SetLink() связывает его с вертикальной позицией и страницей. Этот идентификатор можно передать в Cell(), Write() или Image(), чтобы сделать элемент кликабельным. Внешняя ссылка передаётся строкой, но в деловом документе лучше использовать проверенные адреса и понятный текст, а не выводить длинную техническую строку.
$detailsLink = $pdf->AddLink();
$pdf->SetFont('Helvetica', 'U', 10);
$pdf->Cell(0, 6, 'Go to details', 0, 1, 'L', false, $detailsLink);
$pdf->AddPage();
$pdf->SetLink($detailsLink, $pdf->GetY());
$pdf->SetFont('Helvetica', 'B', 14);
$pdf->Cell(0, 9, 'Details', 0, 1);
Цель можно установить до или после создания ссылки. Если номер страницы не передан, используется текущая. Для оглавления сначала создают идентификаторы разделов, затем в момент вывода заголовка вызывают SetLink(). Номер страницы для печатного оглавления приходится знать отдельно; FPDF не строит структуру заголовков и не рассчитывает оглавление автоматически.
Write() особенно удобен для ссылки внутри предложения: обычный фрагмент печатают стандартным цветом, затем временно включают подчёркивание и синий цвет, передают ссылку и возвращают стиль. Важно восстановить не только цвет, но и начертание, иначе весь последующий текст останется подчёркнутым.
Кликабельная область Cell() равна текстовому элементу внутри ячейки, а Image() может превратить изображение в кнопку. Для доступности не следует полагаться только на цвет: подчёркивание или явная подпись помогают понять назначение при чёрно-белой печати.
Метаданные и начальный режим просмотра
SetTitle(), SetAuthor(), SetSubject(), SetKeywords() и SetCreator() записывают свойства документа. Эти поля видны в сведениях PDF, используются системой поиска и помогают различать автоматически сформированные файлы. Значения поступают из приложения, поэтому их очищают от управляющих символов и задают в согласованной кодировке. Имя файла не заменяет заголовок: после изменения имени файла метаданные сохраняются.
$pdf->SetTitle($documentTitle);
$pdf->SetAuthor($organizationName);
$pdf->SetSubject('Monthly operations report');
$pdf->SetKeywords('operations, report, period');
$pdf->SetCreator('Internal reporting system');
SetDisplayMode() задаёт пожелание для программы просмотра: показать всю страницу, заполнить ширину окна, использовать реальный масштаб, числовой процент или настройки пользователя. Раскладка может быть одиночной, непрерывной либо двухстраничной. Просмотрщик вправе проигнорировать эти параметры, поэтому макет не должен зависеть от начального масштаба.
Для отчёта с мелкой широкой таблицей режим fullwidth помогает при первом открытии, но не делает текст крупнее при печати. Для буклета двухстраничный режим показывает развороты, однако порядок страниц и поля всё равно рассчитываются на уровне шаблона. Не следует использовать режим просмотра как средство скрыть неудобный размер элементов.
SetCompression() включает или отключает сжатие внутреннего представления страниц. Оно активно по умолчанию и требует Zlib; без расширения отключается. Сжатие уменьшает повторяющиеся команды текста и графики, но изображения уже имеют собственное кодирование. Если при отладке нужно изучить внутренние команды, сжатие временно выключают, а перед выпуском возвращают.
Куда направлять готовый документ
Output() поддерживает четыре назначения. I отправляет PDF для показа внутри браузера, если просмотрщик доступен. D заставляет браузер скачать файл. F сохраняет его по локальному пути. S возвращает весь документ строкой. Последний вариант удобен для вложения к письму, загрузки в объектное хранилище или ответа фреймворка, который самостоятельно формирует заголовки.
| Режим | Результат | Практическое применение |
|---|---|---|
| I | Вывод в браузер | Предварительный просмотр сформированного документа |
| D | Принудительная загрузка | Кнопка получения отчёта или квитанции |
| F | Файл по указанному пути | Пакетная генерация и архивирование |
| S | Строка в памяти | Вложение, API-ответ, сохранение через другой слой |
Для I и D третий параметр Output() сообщает, закодировано ли имя файла в UTF-8. Он относится к имени, а не к содержимому страниц. Чтобы избежать несовместимости клиентов, имя делают коротким, исключают слеши и управляющие символы, а пользовательское название пропускают через безопасную функцию.
Режим F не создаёт отсутствующие каталоги. Путь проверяют до Output(), директорию создают с контролируемыми правами, а имя не составляют напрямую из запроса. Для параллельных задач используют уникальное временное имя и атомарное перемещение, чтобы другой процесс не получил наполовину записанный документ.
Режим S расходует память на весь PDF помимо структуры, которую уже держит объект. Для небольших счетов это удобно, но при сотнях страниц и крупных изображениях лучше оценить лимиты. Если последующий API требует строку, снизить память можно оптимизацией изображений и разбиением пакетной работы, но FPDF всё равно формирует документ целиком перед выдачей.
Ошибка о уже отправленных данных
Сообщение о том, что данные уже были выведены, появляется, когда PHP отправил байты до заголовков PDF. Причиной бывает BOM перед открывающим тегом, пробел после закрывающего тега, echo, var_dump, предупреждение или HTML из подключённого файла. Найдите первый вывод, устраните его и не маскируйте проблему постоянной очисткой буфера. Буферизация может помочь контролируемо отбросить диагностический вывод, но предупреждения всё равно нужно исправить.
У чистого PHP-файла, который только генерирует документ, закрывающий тег обычно не нужен. Логи направляют в журнал, а не в тело ответа. Во фреймворке безопаснее получить Output('S') и вернуть строку через штатный объект ответа с Content-Type application/pdf; так управление заголовками остаётся в одном месте.
Практический шаблон счёта
Счёт сочетает фиксированную шапку, реквизиты, таблицу переменной длины, итоги и примечание. Сначала валидируют исходные данные и форматируют даты, количества и деньги. Затем создают страницу, размещают логотип и номер документа, выводят две колонки реквизитов, проверяют место для заголовка таблицы и проходят по позициям.
Широкое описание должно получать остаток после фиксированных столбцов количества, цены и суммы. Для каждой позиции рассчитывают высоту по MultiCell(). Итоговый блок держат вместе: перед ним проверяют место для нескольких строк. Если условия оплаты длинные, их выводят MultiCell() отдельным абзацем. Подписи и печатные линии рисуют только после того, как известен Y конца содержимого.
$pdf->AddPage();
$pdf->SetFont('Helvetica', 'B', 16);
$pdf->Cell(0, 9, 'Invoice ' . $invoiceNumber, 0, 1, 'R');
$pdf->SetFont('Helvetica', '', 9);
$pdf->MultiCell(85, 5, $sellerBlock);
$pdf->SetXY(110, 34);
$pdf->MultiCell(85, 5, $buyerBlock);
$pdf->SetY(max($pdf->GetY(), 70));
renderInvoiceHeader($pdf);
foreach ($items as $item) {
renderInvoiceRow($pdf, $item);
}
renderInvoiceTotals($pdf, $totals);
В примере после двух MultiCell() нельзя полагаться на единственный текущий Y: второй блок стартует с заданной координаты и может быть выше или ниже первого. Надёжный код сохраняет конечный Y каждого блока и берёт максимум. Это правило относится ко всем параллельным колонкам.
Суммы передают уже рассчитанными бизнес-логикой. FPDF не должен решать налоги или округления: его задача — отобразить утверждённые значения. Для контроля полезно сравнить сумму строк и итог перед генерацией, а после создания сохранить хеш файла рядом с записью счёта.
Практический шаблон отчёта
Отчёт обычно содержит титульную часть, краткие показатели, разделы с разной длиной, таблицы и приложения. Подкласс задаёт Header() и Footer(), а отдельные функции отвечают за заголовок раздела, карточку показателя, таблицу и примечание. Каждый компонент начинает с проверки свободного места и завершает работу в предсказуемой позиции.
Заголовок первого уровня можно печатать на новой странице, а заголовок второго — удерживать с первым абзацем. Для этого заранее резервируют высоту заголовка и минимум две строки текста. Между разделами используют Ln(), а не случайные SetY(), чтобы поток оставался понятным. Фиксированные координаты оставляют для шапки, подвала и специально выровненных карточек.
Графики чаще готовят как PNG или JPEG отдельным инструментом и вставляют Image(). Перед генерацией задают одинаковый физический размер, цветовую схему и подписи. Легенда внутри изображения должна быть достаточно крупной для ширины на странице. Если график формируется динамически, его сначала записывают во временный файл с уникальным именем, а после Output() удаляют в блоке finally.
Для приложения с широкими данными добавляют альбомную страницу. Перед ней завершают текстовый поток, вызывают AddPage('L'), рассчитывают ширины по новой GetPageWidth(), а после таблицы возвращаются к портретной AddPage('P'). Header() должен учитывать ориентацию: разделительная линия и правое выравнивание строятся от фактической ширины.
Длинный отчёт тестируют на наборах разной длины: пустом, одном элементе, ровно на границе страницы и с очень длинными значениями. Именно пограничные случаи выявляют осиротевшие заголовки, наложение подвала и строку таблицы, которая не помещается целиком.
Сертификаты, бланки и этикетки
Фиксированный сертификат удобнее строить абсолютными координатами. Размер страницы и ориентация задаются явно, фон при необходимости вставляется изображением, имя получателя центрируется по ширине, а подписи размещаются относительно нижнего края. Перед печатью длинного имени измеряют GetStringWidth(); если оно не помещается, уменьшают кегль в заданных пределах или разбивают на две строки.
Для бланка с заранее напечатанными полями координаты калибруют по контрольной распечатке. Принтер может иметь непечатаемые поля и масштабировать страницу, поэтому в диалоге выбирают фактический размер без подогнать. В шаблоне хранят поправки X и Y, чтобы можно было настроить конкретное устройство без изменения всех координат.
Лист этикеток состоит из сетки. Индекс элемента преобразуют в номер строки и колонки, затем рассчитывают X и Y из полей, размера этикетки и промежутков. Внутри ячейки выводят несколько строк, штрихкод как изображение или расширение и рамку для теста. Перед рабочей печатью рамку отключают.
$column = $index % $columns;
$row = intdiv($index, $columns);
$x = $sheetLeft + $column * ($labelWidth + $gapX);
$y = $sheetTop + $row * ($labelHeight + $gapY);
$pdf->SetXY($x, $y);
$pdf->MultiCell($labelWidth, 4, $labelText, 0, 'L');
Если данные занимают больше мест, чем есть на листе, AddPage() вызывают при переходе индекса через размер сетки. Индекс внутри страницы обнуляют. Длинный адрес не должен вытеснять код за пределы этикетки; для каждого поля задают максимальное число строк и правило сокращения.
Дополнительные сценарии и официальные расширения
Основной класс намеренно невелик. На официальной площадке опубликованы дополнительные скрипты для задач, которых нет в базовом API: этикеток, штрихкодов, кругов и эллипсов, преобразований, расширенных таблиц и других компонентов. Подключение такого файла не делает функцию встроенной: код расширения нужно хранить в проекте, проверить его совместимость и протестировать вместе с обновлением FPDF.
Штрихкод обычно реализуется рисованием полос или созданием изображения. Перед использованием проверяют допустимый набор символов, контрольную сумму, минимальную ширину модуля и тихие зоны. Красивое изображение не гарантирует считывание: тест проводят реальным сканером после печати на целевом устройстве.
Повороты, масштабирование и другие преобразования в расширениях работают через команды графического состояния PDF. Их следует применять парно: начать преобразование, вывести объект, завершить состояние. Незакрытое преобразование влияет на всё последующее содержимое и вызывает труднообъяснимое смещение текста.
Для импортирования страниц существующего PDF базового FPDF недостаточно. Эту задачу решает FPDI: он читает страницу исходного файла и использует её как шаблон, поверх которого FPDF добавляет текст и изображения. Это отдельная зависимость с собственными ограничениями форматов исходного PDF, поэтому её подключают только когда нужен готовый фон, штамп или заполнение формы.
HTML также не является входным языком основного класса. Небольшой пользовательский парсер может преобразовать ограниченный набор тегов в Write(), но полноценные таблицы, CSS, списки и разрывы требуют большого движка раскладки. Если исходное содержание уже подготовлено как сложный HTML, разумнее выбрать библиотеку, созданную для преобразования HTML в PDF.
Ошибки шрифтов и нечитаемые символы
Ошибка Undefined font обычно означает, что SetFont() вызван с семейством или стилем, который не был зарегистрирован AddFont(), либо текст выводится до первого SetFont(). Проверьте точное имя семейства, регистр файла определения и наличие отдельного начертания. Стандартное имя Helvetica доступно без AddFont(), пользовательское — нет.
Could not include font definition file указывает на путь. Выведите абсолютный каталог во временный журнал, сравните его с расположением файла и проверьте права чтения процесса PHP. В системах с чувствительностью к регистру dejavu.json и DejaVu.json — разные имена. После переноса проекта также проверяют, не остался ли FPDF_FONTPATH со старым путём.
Квадраты вместо букв означают, что в самом шрифте нет нужных глифов либо используется неправильная карта кодировки. Искажённая кириллица чаще связана с тем, что UTF-8-строка передана в однобайтовый шрифт без преобразования. Определите кодировку на границе приложения и преобразуйте ровно один раз. Двойное преобразование даёт другой набор мусорных символов.
Разные символы могут иметь одинаковый вид, но различаться кодами: обычный дефис, неразрывный дефис, короткое и длинное тире; обычный и неразрывный пробел; прямые и типографские кавычки. Если карта не содержит символа, замените его осознанно до вывода. Автоматическая потеря символов без журнала опасна для фамилий, сумм и реквизитов.
Для проверки шрифта создают отдельную страницу со строками алфавита, цифрами, знаками валют, кавычками, тире и реальными примерами данных. Это быстрее, чем искать дефект в пятидесятистраничном отчёте. Тестовый PDF сохраняют как контрольный артефакт и сравнивают после изменения шрифтов.
Ошибки изображений и повреждённый результат
Сообщение о неизвестном типе изображения возникает при неподдерживаемом формате или отсутствии расширения, по которому определяется тип. Проверьте сигнатуру файла, а не только имя. Современный веб-ресурс может называться .jpg, но фактически возвращать WebP или HTML-страницу ошибки. Перед Image() откройте файл библиотекой изображений и убедитесь в его размерах.
Повреждённый PDF, который просмотрщик не открывает, часто содержит посторонние байты до сигнатуры или после завершения. Сохраните Output('S') в файл без веб-ответа и сравните. Если локальный файл исправен, проблема в заголовках или middleware. Если повреждён и он, отключите диагностический вывод подключаемых модулей и проверьте предупреждения PHP.
Белая страница не всегда означает отсутствие данных. Текст мог быть белым после SetTextColor(), находиться за пределами листа из-за координат или использовать шрифт без глифов. Временно включите контрастные рамки вокруг блоков, верните чёрный цвет и выведите текущие X/Y в журнал. Не печатайте отладочные координаты в сам PDF, который проверяется на чистоту.
Если картинка выглядит растянутой, задавайте только одну сторону либо рассчитывайте вторую по исходным пропорциям. Если она размыта, сравните пиксельный размер с физическим размером и требуемым dpi. Если файл чрезмерно большой, уменьшите исходные пиксели и качество JPEG; изменение параметра w не перекодирует ресурс.
Ошибки разметки и способы диагностики
Наложение блоков возникает, когда один компонент меняет курсор, а следующий предполагает старую позицию. MultiCell(), Cell() с переносом, Ln(), SetY() и Image() без явного Y имеют разные последствия. Для каждого компонента документируйте входную позицию и место, где он оставляет курсор. После параллельных колонок всегда вычисляйте общий нижний Y.
Съехавшие границы таблицы показывают, что высота строки рассчитана не тем шрифтом или не той шириной. Перед расчётом установите тот же SetFont(), который будет при выводе, и учитывайте внутренний отступ. Явные переводы строки должны увеличивать число строк даже при пустом фрагменте между ними.
Элемент пропал на следующей странице — проверьте автоматический разрыв. FPDF мог добавить страницу перед Cell() или Image(), а код затем вызвал SetXY() с координатами, рассчитанными для старого листа. Для атомарного блока сначала ensureSpace(), затем вывод без промежуточных действий, способных вызвать перенос.
Для диагностики создают режим сетки: рисуют границу печатной области, горизонтальные линии через 5 или 10 мм и рамки компонентов. Режим включается параметром конфигурации и никогда не попадает в рабочий файл. Он быстро показывает неверное поле, лишний Ln() и фактическую высоту MultiCell().
Проверять только первый документ недостаточно. Нужны тестовые наборы с максимальной длиной названия, нулём строк, одной строкой, строкой ровно на границе и количеством, которое создаёт несколько страниц. Снимок контрольного PDF или растровое сравнение страниц выявляет незапланированный сдвиг после изменения кода.
Производительность и размер файла
Основные затраты создают изображения, шрифты и количество элементов. Повтор одного и того же изображения не встраивает копию заново, поэтому логотип в колонтитуле экономичен. Разные масштабированные копии одного исходника лучше выводить из одного файла. Фотографии заранее уменьшают и сжимают; PNG оставляют для графики с прозрачностью и чёткими линиями, JPEG — для фотографий.
Каждое встроенное семейство и начертание добавляет данные. Используйте только реально встречающиеся варианты. Если документ состоит из латиницы и не требует пользовательского шрифта, стандартные семейства дают компактный результат. Для кириллицы размер встроенного ресурса — ожидаемая плата за переносимость.
Сжатие страниц включено по умолчанию и зависит от Zlib. Оно эффективно для текстовых операторов и векторной графики. Отключать его в рабочей выдаче нет смысла, кроме диагностики. Уже сжатый JPEG почти не уменьшится от сжатия потока.
Большой массив строк следует обрабатывать последовательно, не создавая дополнительную копию форматированных данных без необходимости. Однако FPDF хранит страницы до Output(), поэтому память растёт с документом. Для очень крупных архивов разумнее создавать несколько файлов, объединять их подходящим инструментом или переходить на решение с потоковой архитектурой.
Пакетную генерацию выполняют в очереди, ограничивая число одновременных задач. Временные картинки и PDF получают уникальные имена, а очистка запускается даже при ошибке. В журнал пишут идентификатор документа, число страниц, длительность, объём результата и причину отказа, но не секретные поля исходных данных.
Безопасность генерации
FPDF не делает пользовательские данные безопасными автоматически. Текст может содержать управляющие символы, чрезмерно длинные последовательности и неожиданные переносы. Валидация зависит от поля: код ограничивают допустимым набором, имя очищают от управляющих символов, описание ограничивают разумной длиной, а денежное значение форматируют из числа, а не доверенной строки.
Пути к картинкам и файлам нельзя строить прямым соединением пользовательского ввода. Храните разрешённые ресурсы под внутренними идентификаторами, приводите путь к абсолютному и проверяйте, что он остаётся внутри нужного каталога. Для загружаемых изображений проверяйте размер, тип, пиксельные пределы и перекодируйте их.
Имя в Output('F') либо Output('D') очищают от слешей, переводов строк и кавычек. Для локального сохранения предпочтительно генерировать имя на сервере, а отображаемое название хранить отдельно. Права каталога должны позволять запись только нужному процессу; публичная раздача временной папки создаёт риск утечки.
Сам PDF может содержать внешние ссылки. Разрешайте только ожидаемые схемы и домены, если адрес приходит из данных. Внутренние ссылки безопаснее, потому что используют идентификатор AddLink(). Метаданные также очищают: управляющий символ в названии не должен ломать заголовок ответа или журнал.
Файл после генерации можно проверить сигнатурой, ненулевым размером и открытием парсером в отдельном тестовом процессе. Для значимых документов сохраняют хеш, идентификатор шаблона и параметры генерации. Это помогает доказать, какой именно файл был выдан, и повторить результат.
Тестирование шаблонов
Модульный тест проверяет функции форматирования: даты, суммы, кодировки, высоту строк и разбиение данных. Интеграционный тест создаёт настоящий PDF и убеждается, что файл начинается с корректной сигнатуры, имеет ожидаемое число страниц и содержит обязательные метаданные. Для текста можно применить независимый извлекатель, понимая, что порядок символов в PDF не всегда совпадает с визуальным чтением.
Визуальный регрессионный тест рендерит страницы в изображения и сравнивает с эталоном. Допуск нужен для мелких различий сглаживания, но координатный сдвиг, исчезнувшая рамка или новая страница должны обнаруживаться. Эталоны хранят рядом с кодом шаблона и обновляют только после осознанного просмотра.
Минимальный набор данных включает:
- пустые необязательные поля и отсутствие строк таблицы;
- самые длинные допустимые фамилии, названия и адреса;
- кириллицу, цифры, тире, кавычки и знак валюты;
- изображение минимального и максимального разрешения;
- строку, заканчивающуюся точно у нижней границы;
- несколько страниц с повтором Header(), Footer() и шапки таблицы;
- альбомную страницу внутри портретного документа;
- имя файла с пробелами и национальными символами.
Контрольный просмотр выполняют в нескольких распространённых просмотрщиках и на печати. SetDisplayMode() может интерпретироваться по-разному, а тонкие линии и прозрачность зависят от рендерера. Критичный бланк проверяют на том принтере и бумаге, где он будет использоваться.
Сравнение FPDF с аналогами
Выбор библиотеки зависит от исходного представления макета. FPDF удобен, когда документ строится методами и координатами, а шаблон должен быть небольшим и предсказуемым. HTML-движки быстрее осваиваются при готовой веб-вёрстке, более крупные PDF-библиотеки предоставляют Unicode и дополнительные стандарты, а графический редактор нужен для ручного изменения существующего файла.
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| FPDF | Компактных PHP-шаблонов с точными координатами, таблицами и колонтитулами | Unicode, HTML и импорт страниц требуют отдельного решения |
| mPDF | Преобразования UTF-8 HTML и CSS в PDF внутри PHP-проекта | Сложнее и тяжелее для простого координатного шаблона |
| Dompdf | Отчётов, уже свёрстанных как HTML/CSS | Не вся веб-вёрстка и разбиение таблиц воспроизводятся как в браузере |
| TCPDF / tc-lib-pdf | PHP-документов с Unicode, графикой, формами и расширенными возможностями PDF | Объёмный API и более высокая сложность внедрения |
| ReportLab | Программной генерации PDF в Python через Canvas и потоковые компоненты Platypus | Требует Python-окружения и иной архитектуры приложения |
| PDF Commander | Ручного редактирования, объединения и оформления существующих PDF | Не заменяет серверную генерацию документов из PHP-данных |
Для счёта или сертификата, где координаты известны и PHP уже обрабатывает данные, FPDF даёт прямой и прозрачный путь. Для шаблона из HTML с кириллицей практичнее mPDF или Dompdf. Когда нужны формы, расширенная типографика и функции стандартов PDF, следует оценить TCPDF либо современную библиотеку соответствующего класса. ReportLab выбирают в Python-проектах. PDF Commander подходит сотруднику, которому нужно открыть и вручную исправить готовый документ, а не писать генератор.
Когда оставаться на FPDF
Оставаться стоит, если шаблон стабилен, объём кода понятен команде, набор шрифтов контролируется, а новые требования укладываются в Cell(), MultiCell(), Image() и простую графику. Переписывание только ради модного API создаёт риск визуальных расхождений. Сначала измерьте реальные проблемы: время генерации, число ошибок кодировки, стоимость поддержки таблиц и необходимость импортировать страницы.
Когда переходить на другой инструмент
Переход оправдан, когда входом служит сложный HTML, документ содержит много языков, требуется продвинутая разметка, формы, подписи, доступность или изменение существующего PDF. В таком случае накопление расширений вокруг FPDF может оказаться дороже целевого движка. Миграцию проверяют на контрольных документах: сравнивают переносы, размеры шрифтов, таблицы, номера страниц и итоговый объём.
Как организовать код проекта
Генератор лучше отделить от получения данных. Сервис приложения передаёт подготовленную структуру: строки, числа, даты, пути к проверенным изображениям. Класс шаблона отвечает только за разметку. Такой раздел позволяет тестировать бизнес-расчёты без PDF и визуальный шаблон без базы данных.
Повторяющиеся компоненты оформляют методами подкласса: renderSectionTitle(), renderTableHeader(), renderKeyValue(), ensureSpace(). Размеры и поля хранят в свойствах или конфигурации шаблона. Метод компонента полностью задаёт стиль и возвращает курсор в документированное место. Не стоит создавать универсальный рисователь всего с десятками флагов: несколько ясных компонентов легче проверить.
final class InvoicePdf extends FPDF
{
private const LEFT = 15.0;
private const RIGHT = 15.0;
private const BOTTOM = 20.0;
public function sectionTitle(string $text): void
{
$this->ensureSpace(14);
$this->SetFont('Helvetica', 'B', 11);
$this->SetFillColor(238, 238, 238);
$this->Cell(0, 8, $text, 0, 1, 'L', true);
$this->Ln(2);
}
private function ensureSpace(float $height): void
{
if ($this->GetY() + $height > $this->GetPageHeight() - self::BOTTOM) {
$this->AddPage();
}
}
}
Шрифты и статические картинки располагают рядом с шаблоном или в управляемом каталоге ресурсов. Путь строят от __DIR__, а не от текущего рабочего каталога: веб-сервер, CLI и очередь могут запускать один файл из разных мест. Каталог для результата передают отдельно и проверяют.
Ревизию шаблона полезно хранить в данных документа или журнале, но не обязательно печатать на странице. При изменении юридически значимого бланка старые документы должны воспроизводиться. Для этого либо сохраняют готовый PDF, либо сохраняют неизменяемый шаблон и входные данные.
Контрольный список перед выпуском
- Формат, ориентация, поля и нижний резерв заданы явно.
- SetFont() вызывается до любого текста, а нужные начертания зарегистрированы.
- Кодировка входных строк соответствует подготовленным шрифтам.
- Длинные значения проверены в Cell() и переведены в MultiCell() там, где нужен перенос.
- Высота многострочных строк таблицы рассчитана до вывода.
- Header(), Footer() и повторная шапка таблицы не пересекаются с основным содержимым.
- Изображения имеют проверенный формат, разумный пиксельный размер и доступный путь.
- После цветных компонентов восстановлены цвет текста, заливки и обводки.
- Output() выбран по сценарию, а до вывода в браузер нет посторонних байтов.
- Имя и путь файла очищены, каталог существует и доступен для записи.
- PDF открыт в нескольких просмотрщиках и проверен на печати.
- Пограничные наборы данных прошли визуальный регрессионный тест.
Этот список особенно важен после небольшого изменения, которое кажется локальным. Новый шрифт меняет ширину строк, дополнительная колонка влияет на разрывы, а увеличенный логотип может сдвинуть шапку на всех страницах. Проверка должна охватывать документ целиком, а не только изменённый фрагмент.
Итоговый подход к работе
Надёжный шаблон FPDF строится не набором случайных координат, а последовательностью компонентов с известными размерами и правилами перехода. Сначала задаются страница и поля, затем шрифты и кодировка, после этого компоненты выводят содержимое и контролируют свободное место. Точные координаты применяются там, где форма фиксирована, а потоковые методы — там, где длина текста меняется.
Cell() остаётся основой коротких строк и таблиц, MultiCell() решает переносы, Write() собирает текучие фрагменты, Image() добавляет растровые ресурсы, а Header(), Footer() и AcceptPageBreak() управляют многостраничной структурой. Метаданные и Output() завершают документ и передают его в нужный канал. Когда ограничения кодировок, HTML-разметки или импорта существующих страниц становятся центральными, лучше подключить специализированное расширение или выбрать движок, созданный для такой задачи.
Перед использованием рабочего файла проверьте его на реальных данных, включая самые длинные строки и границы страниц. Сохраняйте контрольные примеры, измеряйте результат после изменений и держите бизнес-расчёты вне класса разметки. Тогда генерация остаётся воспроизводимой, ошибки быстро локализуются, а один шаблон уверенно обслуживает как одиночную выдачу, так и пакетное формирование документов.