Доверьте свой бизнес профессионалам!

Как сгенерировать pdf-файл: Битрикс, php и TCPDF

Частенько клиенты подкидывают нам новые интересные задачки. Вот намедни бизнес-школа обратилась с просьбой сделать им на сайте конструктор, который по заданным параметрам формирует пользователю pdf-файл со списком курсов. Рассказываю, как это было.

Итак, задача:
Создать конструктор курсов и тренингов для сайта на CMS 1С-Битрикс. В конструкторе: выбор временного промежутка, интересующих рубрик и города проведения. Пользователь выбирает параметры, на их основе формируется pdf-файл со списком всех семинаров и курсов, которые проводятся в эти даты. Курсы разбиты по городам и направлениям. Из файла можно перейти напрямую к описанию курса и сделать заказ на сайте.
Вот, что получилось https://www.itctraining.ru/raspisanie/
Теперь поговорим непосредственно о технической стороне вопроса.

План работы.

Я наметила себе, какие конкретно шаги должны быть проработаны для реализации поставленной задачи.

1. Создание непосредственно формы конструктора.
2. Передача полученных данных при помощи ajax в файл обработки.
3. Выборка из всех элементов на сайте только тех, которые отвечают заданным условиям (дата, рубрики, город проведения).
4. Генерация PDF-файла в стиле сайта и его отображение пользователю.

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

Выбор библиотеки.

  • Пожалуй, самая популярная библиотека - PDFlib. Но она по большей части коммерческая. А бесплатная версия недостаточно гибкая. Поэтому от этой идеи пришлось отказаться.
  • Оказалось, что библиотека FPDF не дружит с юникодом и кириллицей. Поэтому этот вариант тоже мимо, поскольку сайт на Битриксе, а значит в UTF-8.
  • Поэтому выбор мой пал на TCPDF. Это открытая PHP библиотека для генерации PDF, которая включает полную поддержку Unicode UTF-8. Проста в использовании, гибко настраивается, - в общем идеальный выбор в данных условиях! Источник - https://tcpdf.org/
Как установить библиотеку TCPDF.

Здесь никаких сложностей не возникло, процесс стандартный. Скачиваем из источника - устанавливаем строкой
require_once('tcpdf/tcpdf.php');
Здесь один важный момент. Иногда в процессе формирования pdf-файла вы  можете увидеть в консоли ошибку:
TCPDF ERROR: Some data has already been output, can't send PDF file
В таком случае перед подключением нужно вставить конструкцию:
ob_end_clean();

Кириллица и TPDF.

Библиотека поддерживает кириллические шрифты, все отлично. Но, как оказалось, не все так однозначно. Ситуация осложнялась тем, что заказчик хотел, чтобы в файле использовался тот же самый шрифт, что и непосредственно на сайте. Заглянем в документацию. Там сказано, что TCPDF автоматически преобразует шрифты в нужный формат с использованием метода addTTFfont(). Иными словами, вам нужно загрузить необходимый шрифт в формате ttf на сайт, и воспользоваться конструкцией
//указываем путь к файлу
$font = $_SERVER["DOCUMENT_ROOT"] . '/tcpdf/fonts/segoeui.ttf';

// преобразуем шрифт
$fontname = $pdf->addTTFfont($font, 'TrueTypeUnicode', '', 96);

// устанавливаем шрифт для всего файла
$pdf->SetFont($fontname, '', 14, '', false);
Если вдруг у вас так и не получилось подключить шрифт встроенным методом, на помощь приходят онлайн конвертеры шрифтов. Можете воспользоваться, например, этим http://fonts.snm-portal.com/ . Грузите туда шрифт, а преобразованный файл кидаете в папку со шрифтами. Готово!

Создаем шапку и футер pdf файла.

Требовалось, чтобы сгенерированный pdf файл выглядел как на фирменном бланке. Соответственно, нужны шапка с логотипом и контактами. И футер с дополнительной информацией.
// Создаем шапку и футер
class MYPDF extends TCPDF {

    //Шапка
    public function Header() {
        // Логотип
        $image_file = $_SERVER['DOCUMENT_ROOT'] .'/img/logo.png';
        $this->Image($image_file, 10, 10, 15, '', 'PNG', '', 'T', false, 400, '', false, false, 0, false, false, false);

        // Шрифт и цвет текста
        $this->SetFont('segoeui', '', 8);
        $this->SetTextColor(101, 101, 101);

        // Контакты
        $contacts = 'Здесь будут контакты';
        $this->writeHTML($contacts, true, 0, true, 0);
    }

    // Футер
    public function Footer() {
        // Устанавливаем отступ от нижнего края страницы
        $this->SetY(-15);

        // Шрифт и цвет текста
        $this->SetFont('segoeui', 'I', 8);
        $this->SetTextColor(101, 101, 101);

        //Дополнительная информация
        $info = '<span style="text-align:center;">Информация актуальна на '.date("d.m.Y").'.</span>';
        $this->writeHTML($info, true, 0, true, 0);

        // Номер страницы        
        $this->Cell(0, 10, 'Страница '.$this->getAliasNumPage().'/'.$this->getAliasNbPages(), 0, false, 'C', 0, '', 0, false, 'T', 'M');    
     }
}
Если для вашего файла не требуются шапка и футер, то можно воспользоваться конструкцией
// выключаем шапку и футер
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
Создание pdf файла.

Теперь можно перейти непосредственно к созданию файла.
//Если мы использовали класс MYPDF для создания шапки и футера
$pdf = new MYPDF('P', 'mm', 'A4', true, 'UTF-8');

//Если шапка и футер нам не нужны
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8');
Используя библиотеку TCPDF, можно формировать файл из HTML кода. Сразу уточнение: в стилях css можно устанавливать цвет, границы,стиль текста. Позиционирование объектов (margin, padding) - не работают. Но отступы можно настраивать при помощи встроенных методов.
Идем дальше
  //Основная информация о файле
  $pdf->SetCreator('Создатель');
  $pdf->SetAuthor('Автор файла');
  $pdf->SetTitle('Название файла');
  $pdf->SetSubject('Тема');
  $pdf->SetKeywords('Ключевые слова');

  //Устанавливаем отступы от края для всех страниц (слева, сверху, справа, снизу)
  $pdf->SetMargins(10, 40, 10, 10);

  // Устанавливаем шрифт, который будет использоваться в документе
  $pdf->SetFont('segoeui', '', 12, '', false);

  $pdf->AddPage(); // Добавляем страницу
  $pdf->SetDrawColor(210, 100, 0); // Установка цвета (RGB)
  $pdf->SetTextColor(71, 71, 71); // Установка цвета текста (RGB)
Теперь нужно продумать и создать структуру документа в html и вывести файл. Можно пользоваться различными методами, все они доступно описаны в примерах на сайте разработчика. Для вывода html нас интересуют 2 из них.
// writeHTML($html, $ln=true, $fill=false, $reseth=false, $cell=false, $align='')
// writeHTMLCell($w, $h, $x, $y, $html='', $border=0, $ln=0, $fill=0, $reseth=true, $align='', $autopadding=true)
Ну и собственно вывод файла
$pdf->Output($_SERVER['DOCUMENT_ROOT'] .'/file.pdf', 'FI'); // Сохранить и вывести в браузер
Проблема №1. Разрыв таблицы в pdf файле.

В моем файле должна располагаться достаточно длинная таблица. Но проблема в том, что она могла оборваться в середине ячейки и перепрыгнуть на новую страницу. Читабельность такого файла, естественно, затруднена. Можно было написать собственную функцию по обходу всех данных на первом шаге с замером высоты строк и формированием файла на втором. Однако, такая идея была отклонена. Данных на сайте много, если пользователь захочет сформировать расписание курсов по многочисленным тематикам на год вперед, то обходить придется слишком много данных. А это низкая скорость формирования. Поэтому пришла следующая идея.
   //После записи в файл строки получаем текущую позицию по вертикали   
   $point = $pdf->GetY();  

   //Формат листа A4, а это 297 мм в высоту. Самая минимальная строка с данными - в высоту примерно 50. Следовательно, если даже маленькая строка не помещается на страницу - переходим на следующую.    
   if (($point + 50) > 297){
        $pdf->AddPage();
    }
Этот простой код сделал файл аккуратным и удобным для чтения.

Проблема №2. Как открыть сгенерированный библиотекой TCPDF файл в новом окне после выполнения ajax запроса.

Заглянем в документацию. TCPDF поддерживает несколько вариантов формирования файла.
I: отправка встроенного файла в браузер (по умолчанию). Плагин используется, если он доступен. Имя, указанное по имени, используется при выборе опции "Сохранить как" в ссылке, генерирующей PDF.D: отправить в браузер и принудительно загрузить файл с именем, заданным по имени.F: сохранить в файл локального сервера с имя, данное по имени.S: возвращает документ в виде строки (имя игнорируется).FI: эквивалент опции F + IFD: эквивалент опции F + DE: возврат документа в виде вложения электронной почты base64 MIME multi-part (RFC 2045)
Мне нужно было, чтобы файл просто открывался в новой вкладке. Сразу скажу, без сохранения это сделать не удалось. Но решилась проблема вот как.
$(document).ready(function(){

   //Передаем данные в ajax
   $( "#form" ).submit(function (event){
      $('.preloader').show(); //Отображаем прелоадер
      event.preventDefault(); //Не перезагружаем страницу
      let now = Date.now(); //Получаем дату и время в формате TIMESTAMP
      $.ajax({
         type: 'POST',
         url: '/ajax.php',
         data: {
            .......... //Здесь передаем данные из формы
            "date":now, //Передаем текущие дату и время
         }
         }).done(function(response) { //Если запрос успешно отработал
            //console.log(response);
            window.open('/files/file' + now + '.pdf','_blank'); //Открываем в новом окне файл, созданный TCPDF
            $('.preloader').hide(); //Прячем прелоадер
            window.setTimeout(function () { //Через 5 секунд удаляем файл
               dataString2 = 'Downloaded=true';
               $.ajax({
                  type: 'post',
                  url: '/delete.php',
                  data: dataString2,
               });
            }, 5000);

         }).fail(function(response) {
            console.log(response);
         });
      });

})
//В файле ajax.php сохраняем сгенерированный файл с приставкой даты и времени
$pdf->Output($_SERVER['DOCUMENT_ROOT'] .'/files/file'.$_REQUEST["date"].'.pdf', 'FI'); 
Таким образом, TCPDF сохраняет файл на сервере и отдает его пользователю. А delete.php содержит скрипт, который удаляет файл. И место экономится, и пользователь доволен!

Вот так удалось справиться с поставленной задачей. Хотите такой же конструктор? Или может быть лучше? Вы знаете, кому звонить)

Разработчик - это в крови. Люблю Битрикс, нестандартные задачи и интересные проекты.


© 2021 Digital-агентство полного цикла "НастАртВЕБ"