При работе над веб-проектом иногда возникает необходимость генерировать PDF-файлы с большими таблицами: прайс-листы на тысячи позиций. Нашлись разные библиотеки для генерации PDF-файла из PHP-скрипта:

• FPDF
• MPDF — основанная на FPDF библиотека, позволяющая генерировать pdf-файл из любого html-кода
• DOMPDF
• TCPDF

и многие разные другие библиотеки. Наиболее мощной и подходящей, к тому же, изначально корректно работающей с кириллицей, оказалась библиотека MPDF, если бы не критичный в нашем случае недостаток: большие таблицы и вообще большие файлы крайне медленно генерировались. Более того, часто генерация не происходила совсем, а скрипт останавливался с ошибкой 504.

Дальнейший поиск помог найти программу wkhtmltopdf. Сайт программы: http://wkhtmltopdf.org.

В отличие от php-библиотек, это серверная программа, распространяемая в том числе в виде пакетов и исполняемых файлов для linux, windows и других операционных систем. Программа принимает html-код (в виде веб-адреса, пути к файлу либо строки кода) и генерирует на его основе pdf-файл на сервере.

Предварительный опыт показал, что на локальном сервере XAMPP под Windows огромная html-таблица на 300-500 страниц преобразуется в pdf-файл за 1-2 секунды!

Установка wkhtmltopdf на CentOs 6
Для работы программе необходим webkit и qt.

Итак, установим требуемое окружение и программу на сервер. На нашем сервере установлена CentOs 6. Зайдем на сервер с правами root и выполним следующие команды.

Получим rpm-пакет программы wkhtmltopdf по ссылке с сайта разработчика и установим ее на рабочем сервере:

wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-centos6-i386.rpm yum --nogpgcheck localinstall wkhtmltox-0.12.2.1_linux-centos6-i386.rpm

Все зависимости пакетов должны автоматически провериться и удовлетвориться. Если окружение по каким-то причинам не установилось, используйте команды:

yum install urw-fonts libXext openssl-devel libXrender yum install xorg-x11-fonts-cyrillic.noarch xorg-x11-fonts-misc.noarch xorg-x11-fonts-truetype.noarch xorg-x11-fonts-100dpi.noarch xorg-x11-fonts-75dpi.noarch fonts-ISO8859-2.noarch fonts-ISO8859-2-100dpi.noarch fonts-ISO8859-2-75dpi.noarch freefont.noarch

До недавнего времени программа не предоставлялась в виде rpm-пакета, и приходилось копировать бинарный файл и вручную устанавливать все необходимые пакеты.

Использование wkhtmltopdf на CentOs 6
Общий формат запуска программы такой:

wkhtmltopdf <путь, имя исходного файла.html> <путь, имя выходного файла.pdf>

Кроме того, программа позволяет автоматически встраивать шапку и подвал документа из отдельных html-файлов. Для этого синтаксис такой:

wkhtmltopdf --header-html <путь,имя шапки.html> --footer-html <путь,имя подвала.html> <путь,имя исходного файла.html> <путь, имя выходного файла.pdf>

Также среди опций запуска программы — настраиваемый размер полей получаемого pdf-файла. В верхнее и нижнее поле программа подставляет шапку и подвал:

wkhtmltopdf --margin-top 35mm --margin-bottom 27mm --margin-left 10mm --margin-right 10mm --header-html <путь,имя шапки.html> --footer-html <путь,имя подвала.html> <путь,имя исходного файла.html> <путь, имя выходного файла.pdf>

В этом примере:
• верхнее поле: 35 мм
• нижнее поле: 27 мм
• левое, правое поля: по 10мм

Приведу также пример кода подвала. В нашем случае автоматически формируются и подставляются в подвал номера страниц. Таким образом, в нашем документе автоматически пронумеруются страницы:

<html><head><script> function subst() { var vars={}; var x=document.location.search.substring(1).split('&'); for (var i in x) {var z=x[i].split('=',2);vars[z[0]] = unescape(z[1]);} var x=['frompage','topage','page','webpage','section','subsection','subsubsection']; for (var i in x) { var y = document.getElementsByClassName(x[i]); for (var j=0; j<y.length; ++j) y[j].textContent = vars[x[i]]; } } </script></head><body style="border:0; margin: 0;" onLoad="subst()"> <div align="right" style="font-family:'Times New Roman', Times, serif; font-size: 14px;"> /<span class="page"></span>/ </div> </body></html>

Также среди полезных опций запуска программы:

— encoding – указание кодировки исходного html-файла, например:
--encoding windows-1251

— page-size – указание формата страницы, например:
--page-size A4

— orientation – ориентация страницы, например:
--orientation Landscape

В нашем веб-проекте на php-странице, формирующей pdf-файл, используется такой php-код:
$tmp=time(); $f=fopen(ABSPATH.'/tmp/'.$tmp.'.html','w'); fputs($f, $llg); fclose($f); $cd = "cd ".ABSPATH.'/tmp'; exec($cd); $command = "wkhtmltopdf-i386 --margin-top 35mm --margin-bottom 27mm --margin-left 10mm --margin-right 10mm --footer-html ".ABSPATH."/tpl-sm/pl_pdf/pdf_footer.html --header-html ".ABSPATH."/tpl-sm/pl_pdf/pdf_header.html ".ABSPATH.'/tmp/'.$tmp.'.html'." ".ABSPATH.'/tmp/'."$tmp.pdf"; exec($command); if (file_exists(ABSPATH.'/tmp/'.$tmp.'.pdf')) { header('Content-type: application/pdf'); header('Content-Disposition: attachment; filename="pricelist.pdf"'); readfile(ABSPATH.'/tmp/'.$tmp.'.pdf'); } unlink(ABSPATH.'/tmp/'.$tmp.'.pdf'); unlink(ABSPATH.'/tmp/'.$tmp.'.html');

В этом коде:
• переменная $llg — содержит html-код прайс-листа
• константа ABSPATH — абсолютный путь к папке веб-проекта на сервере.
Код делает следующее:
• Записывает во временный файл html-код прайс-листа;
• Переходит во временный каталог;
• Запускает wkhtmltopdf с требуемыми опциями;
• Если pdf-файл был успешно создан — то возвращает его пользователю в браузер, предлагая скачать файл под именем pricelist.pdf;
• Удаляет временные html- и pdf-файлы из временного каталога.

Комментарии (24)


  1. Blumfontein
    10.09.2015 07:28
    +1

    По-моему, wkhtmltopdf можно из композера устанавливать. И для использования тоже врапперы есть, консоль вызывать неудобно.


    1. hudson
      10.09.2015 21:45

      Это враппер к утилите wkhtmlto*. Сам инструмент — это консольные приложения.


  1. Obramko
    10.09.2015 08:57

    изначально корректно работающей с кириллицей

    Не думал, что в мире UTF-8 для кого-то это до сих пор проблема.


    1. Diden05
      10.09.2015 11:21
      +2

      Вы удивитесь, но проблем с кодировками по прежнему много, тяжкое наследие так сказать.


    1. gydex
      15.09.2015 18:34

      При работе с FPDF и подобными библиотеками приходилось конвертировать русские ttf-шрифты в удобный ей формат, чтобы кириллица заработала в принципе. В MPDF это уже было сделано изначально.


  1. eugenyh
    10.09.2015 11:22
    +2

    Программа принимает html-код (в виде веб-адреса, пути к файлу либо строки кода) и генерирует на его основе php-файл на сервере.
    может PDF?


  1. schors
    10.09.2015 12:08

    1. Эмуляцией браузера по прайслисту… А пробовали не на пустой машине, а на VDS запустить? Вы же понимаете, что практически браузер запускаете?
    2. Держать прайс на несколько тысяч позиций в переменной — это план победы. Когда он станет под миллион позиций — что делать будете?
    3. Имея прайс текстом в одной переменной были проблемы с переводом его в UTF-8? Т.е. функция iconv на переменную не?
    4. Вы вот так прямо «запрос-ответ» генерируете нечто большое? А если связь отвалится — будете перегенерировать? Кормите девопсов, которые потом будут предлагать вам облака для решения несуществующей проблемы? :) Не, я тоже генерю иногда на лету, но на 5-6 страничек, которые мне не жалко перегенерить.
    5. Кстати, а как прайс становится HTML? Т.е. Вы ещё готовите специальный HTML, который можно превратить в читаемый прайс, коий HTML явно надо делать отдельно от того, что людям показывается. Ну т.е. в каких-то частных случаях этим действительно можно пренебречь, но… Изучаете конвертилку из HTML в PDF, включая шаблоны и вот всякие ключи… Чую бесовщину, обосновать не могу.
    6. А LaTeX не? Пакет texmf сейчас вполне развит, даже TTF туда впихивается за милу душу. Генерация текста/таблиц может быть автоматизирована по самое небалуйся. Причем, если без изысков, то и изучать ничего изыскового не надо. Там сразу Вам и разбиение таблиц по страницам, и игра с текстом, и нумерация без явапрограмминга, и в общем чёрт в ступе. Два дня покурить над самим LaTeX, и потом всё делается быстрее, чем HTML-шаблоны.


    1. gydex
      15.09.2015 18:40

      Мы решали задачи конвертации прайса с тысячами позиций. Миллиона там не предполагалось. Будет миллион — будем генерировать в фоне, задействуем LaTeX. На VDS кстати еще быстрее, чем на пустой машине, работает.
      Есть два варианта печатной формы прайс-листа: HTML и PDF. Обе показываем людям, кому в чем удобнее, в том и смотрят. PDF-версия фактически формируется из шаблона для HTML-версии. Соответственно, быстрее тиражируются изменения шаблона, если они требуются.


  1. PQR
    10.09.2015 12:37
    +1

    Я длительное время использовал MPDF, но он медленный, поэтому изучал альтернативы, тестировал разные варианты. Ещё один интересный сопособ: рендрить с помощью Node и PhantomJS: www.npmjs.com/package/phantomjs-pdf

    Но я в итоге остановился на wkhtmltopdf. Расскажу свой опыт.

    Во-первых, есть готовая обёртка на PHP: github.com/mikehaertl/phpwkhtmltopdf (сам ей не пользуюсь, у меня простая задача, все аргументы зафиксированы, нет смысла в этой обёртке)

    Во-вторых, генерировать временный html файл на диске не обязательно, wkhtmltopdf умеет принимать исходный html через stdin, если указать параметр "-" (прочерк). На PHP получается как-то так:

    $descriptorspec = [
        0 => ['pipe', 'r']; //stdin
        1 => ['pipe', 'w']; //stdout
        2 => ['pipe', 'w']; //stderr
    ];
    
    //Обратите внимание на аргументы, которые я передаю при запуске wkthmltopdf 
    //--disable-smart-shrinking без этого аргумента, всё становится каких-то не правильных пропорций
    //--dpi 96 если принудительно не поставить dpi, то размеры указанные в css в милиметрах на печати будут совсем не такими!
    //- последний аргумент это прочерк, чтобы передать html через stdin
    //bypass_shell (только для Windows): при установке в TRUE процесс будет запущен в обход оболочки cmd.exe
    //это нужно для обхода бага https://bugs.php.net/bug.php?id=60181 "proc_open fails to read quoted whitespaced directories in Windows"
    $process = proc_open("/path/to/wkthmltopdf --disable-smart-shrinking --margin-left 20mm --dpi 96 -", $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
    
    if (is_resource($process)) {
        
        //Пишем в stdin html контейнт
        fwrite($pipes[0], $html);
        fclose($pipes[0]);
    
        //Читаем результаты и ошибки, если они вам нужны в приложении
        $stdOut = stream_get_contents($pipes[1]);
        $stdErr = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);
    
        $exitCode= proc_close($process);
    
    }
    


    Более подробно про разные параметры принимаемые wkthmltopdf рекомендую почитать здесь: wkhtmltopdf.org/usage/wkhtmltopdf.txt

    Да, если нужен header-html и footer-html, то пара временных html файлов на диске всё-таки понадобится.

    В третьих, на Linux машинах есть неприятный баг, что шрифты выглядят меньше, чем при печати в PDF этого же html файла с помощью браузера: github.com/wkhtmltopdf/wkhtmltopdf/issues/2171
    По этому багу, в том числе, есть много обсуждений на StackOverflow, но фикса в ближайшее время не предвидится, предлагается workaround: github.com/wkhtmltopdf/wkhtmltopdf/issues/2171#issuecomment-139164039

    Но я использую другой подход: есть отдельный windows сервер, на котором запускается wkthmltopdf, а обращение идёт к нему через простенький самописный веб-сервис: принимает html контент в POST и возвращает готовый PDF — так и живём.

    Вообще по wkhtmltopdf активность на github почти нулевая, баги не фиксятся, но ничего лучше пока не нашел. Кто знает C++ — присоединяйтесь к проекту, внесите свой вклад в opensource!

    Есть ещё www.princexml.com — но он платный и жутко дорогой!


    1. Fesor
      10.09.2015 13:41

      Главная проблема wkhtmltopdf — старый webkit, большая часть багов связана именно с этим. ветка 0.12 основана на 4-ом QT и соответственно довольно древней версии webkit. Ветка 0.13 же основана на Qt 5.4 и, если удалось собрать и оно не крэшнулось, то там все получше.

      Веселье в ном что альтернативы нет, можно пробовать запустить хром скажем и распечатать страницу через селениум какой, но это извращение то еще. Штуки же типа phantomjs (сейчас вышла вторая версия, использующая Qt 5.4) используют для генерации скриншетов и рендринга PDF код wkhtmltopdf, так что всеравно придется ждать.

      принимает html контент в POST и возвращает готовый PDF — так и живём.

      Вариант с windows сервер худо-бедно живой, если у вас он есть. Вот только кастыли с RPC можно было бы заменить на какой-нибудь MQ, например RabbitMQ


    1. schors
      10.09.2015 14:55

      А смысл в этих телодвижениях, если есть вполне живой LaTeX? Там тоже есть какие-то баги и всё такое, но вот туда бы как раз влить усилия.


      1. Alexufo
        10.09.2015 17:26

        смысл в html с css3 с поддержкой svg — так генерируем бейджи для печати со штрихкодами.


        1. schors
          10.09.2015 17:31

          А зачем? Он не предназначен для этого ) Сколько я не пытался шаблонизировать для PDF, какими только библиотеками — один фиг изучаешь особенности конкретного рендера. А смысл? Для того же LaTeX есть просто шаблон для штрихкодов. И делается это проще. И хорошо когда одна страница и не надо программировать в HTML многостраничность таблицы например. И опять же — HTML это немного не типографская штука…


          1. Alexufo
            10.09.2015 17:42

            Что то не понял) Вы про вынесение временных файлов на диск перед сборкой? В нашем случае мы выносим. Проблем с бейджами, делениями на листы для двухсторонней печати (бейдж на другой скан паспорта) на принтере нет. php ->html->wkhtmltopdf->pdf все гладко и относительно удобно. Штрихи генерятся на php в svg. Уж зачем Latex нужен вот не пойму :-) Помоему это шаг назад.
            Удобнее wkhtmltopdf ничего и нет, увы.


            1. schors
              10.09.2015 17:55

              Зачем как шаблон использовать HTML? В чём смысл? Опять же. Если это одноразовая задача — не вопрос, смысл ясен. Но если нет, то зачем? LaTeX шаг назад? :) Тогда UTF-8 тоже. Его придумали два стрика для своего недоюникса в самом начале 90-ых, который так и не выстрелил. И /procfs кстати тоже. Про HTML, который ножницами вырезали из IBM SGML 70-ых я и вообще молчу :) И про PDF, который есть расширенная версия PS, который где-то в районе 80-ых застрял, ктсати тоже :) LaTeX вполне жив в различных видах, развивается и до сих пор вполне используется для различной верстки. Имеет кучу всяких шаблонов и расширений, вполне вменяемых. Изначально служил и служит для автоматизации создания книг, брошюр, листовок и всё такое. Прямо вот для него работа. Там сложно начать, но при этом он прост. Для половины задач есть готовые решения. Усложнения задач проходят мягко и хорошо. Рендеринг однозначный, в отличии от. Не требует подпорок для большинства задач. Иногда надо поднапрячься, чтобы придумать как верно на нем что-то сделать, но это делается, как я уже говорил — без подпорок.


              1. Alexufo
                10.09.2015 18:03
                +2

                потому html везде в вебраработке и бейджики на нем сделать не проблема. Latex шаг назад в вебразработке в скорости изменения макета. Конечно, на LaTeX много кто верстает без проблем, но для бейджиков прикручивать его ну уж совсем лищнее коли на wkhtmltopdf все устраивает. Тут все просто — если все устраивает то оно отлично: -)
                Если задачи будут куда сложнее, то LaTeX можно рассматривать.


                1. schors
                  11.09.2015 10:27

                  Ну я ранее вроде сказал, что если только бэджик, или там бэджик, счет и акт — то фиг с ним. Но попробуйте просто для себя (из любопытства) заморочиться — я Вас уверяю, там изменение макета будет намного быстрее, чем типогравская подгонка XML-подобного языка :) Не обязательно кстати даже ставить ПО — есть какое-то количество онлайн-сервисов редактирования. Там принцип в два окна — в левом пишешь, в правом показывается итоговый pdf. Понравится — поставите ПО уже себе для автоматизации.


                1. schors
                  11.09.2015 10:28

                  Однако, у топикастера немного другая проблема и он там бегает вокруг листа с подпорками и свистелками. И вот ему бы как раз и заморочиться.


    1. gydex
      15.09.2015 18:45

      Пример интересный.
      Из всего, что пробовали, wkhtmltopdf самая быстрая и корректная утилита. А header-html и footer-html нужны, там элементы фирменного бланка с графикой.


  1. sHinE
    10.09.2015 21:36

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


    1. Londeren
      11.09.2015 10:51

      Может кто-то знает исправлено это сейчас или нет? Столкнулся с такой же проблемой в dompdf (таблицы с colspan rospan), все воркараунды некрасивые


      1. gydex
        15.09.2015 18:26

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


  1. DmitryKoterov
    11.09.2015 02:46
    +1

    exec($cd); — это пять. Можете смело удалять данную строку, она ничего не делает.


    1. gydex
      15.09.2015 18:50

      Согласен, в этом примере эта команда лишняя.