Создаем документ PDF из PHP скрипта с помощью библиотеки FPDF. Сравнение способов генерации pdf из html Создаем колонтитул страницы и вводный текст




Одной из важных частей любой системы управления проектами является возможность сделать отчёт о текущем положении дел. Это позволяет точнее оценивать текущее состояние дел. Для пользователей такой отчёт может быть не только аналитикой нужной для работы, но и своеобразной формой отчётности. Поэтому в компании Revizto уделяют большое внимание технической стороне генерации отчётов.

У отдельных клиентов отчёты представляют из себя большие pdf на десятки тысяч страниц. Само собой такие отчёты весят гигабайты и генерация их занимает продолжительное время. Столкнувшись с очередным ростом размера отчёта, мы решили проанализировать нашу архитектуру генерации pdf.

Традиционно в вебе генерация pdf состоит из двух этапов:

  1. Генерация html страницы, которая потом станет pdf
  2. Перевод html в pdf

Первый этап в нашем случае занимал достаточно мало времени, а вот второй мог длиться больше часа. Поэтому мы сделали небольшое сравнение какие существуют опенсорсные возможности по генерации pdf:

    Command being timed: "phantomjs --ssl-protocol=any ./capture.js 1.html 1.pdf" User time (seconds): 2240.56 System time (seconds): 6.96 Percent of CPU this job got: 83% Elapsed (wall clock) time (h:mm:ss or m:ss): 44:35.89 Average shared text size (kbytes): 0 Average unshared data size (kbytes): 0 Average stack size (kbytes): 0 Average total size (kbytes): 0 Maximum resident set size (kbytes): 1821840 Average resident set size (kbytes): 0 Major (requiring I/O) page faults: 28 Minor (reclaiming a frame) page faults: 489364 Voluntary context switches: 53462 Involuntary context switches: 31797 Swaps: 0 File system inputs: 4576 File system outputs: 233848 Socket messages sent: 0 Socket messages received: 0 Signals delivered: 0 Page size (bytes): 4096 Exit status: 0

    Command being timed: "/usr/bin/google-chrome-stable --headless --disable-gpu --print-to-pdf 1.html" User time (seconds): 54.22 System time (seconds): 7.32 Percent of CPU this job got: 5% Elapsed (wall clock) time (h:mm:ss or m:ss): 18:49.74 Average shared text size (kbytes): 0 Average unshared data size (kbytes): 0 Average stack size (kbytes): 0 Average total size (kbytes): 0 Maximum resident set size (kbytes): 951796 Average resident set size (kbytes): 0 Major (requiring I/O) page faults: 2 Minor (reclaiming a frame) page faults: 938614 Voluntary context switches: 184497 Involuntary context switches: 37463 Swaps: 0 File system inputs: 368 File system outputs: 174352 Socket messages sent: 0 Socket messages received: 0 Signals delivered: 0 Page size (bytes): 4096 Exit status: 0

  1. Wkhtmltopdf Command exited with non-zero status 1 Command being timed: "wkhtmltopdf 1.html 1.pdf" User time (seconds): 3356.13 System time (seconds): 7.12 Percent of CPU this job got: 99% Elapsed (wall clock) time (h:mm:ss or m:ss): 56:21.54 Average shared text size (kbytes): 0 Average unshared data size (kbytes): 0 Average stack size (kbytes): 0 Average total size (kbytes): 0 Maximum resident set size (kbytes): 3457744 Average resident set size (kbytes): 0 Major (requiring I/O) page faults: 8 Minor (reclaiming a frame) page faults: 977440 Voluntary context switches: 38567 Involuntary context switches: 47975 Swaps: 0 File system inputs: 1240 File system outputs: 197480 Socket messages sent: 0 Socket messages received: 0 Signals delivered: 0 Page size (bytes): 4096 Exit status: 1

Из преведенного теста видно, что лучший результат получается у chrome, потом в два раза хуже phantomJs, а потом ещё значительно хуже Wkhtmltopdf. Этот результат перекликается с проблемами с разработкой phantomJs

C другой стороны chrome зависит от X11, так что на сервере вопросов с ним будет значительно больше чем с phantomJs.

Если делать небольшой вывод, то для тестирования машине с десктопом chrome безусловный победитель. На сервере его разумно использовать, если не бояться поставить туда X11, иначе phantomJs будет оптимальным вариантом

PDF24 Creator, это бесплатный PDF генератор для вашего ПК. Генерация PDF файлов невероятно простая задача с этой программой. Комбинация дополнительных возможностей вместе с созданием PDF файлов, делает этот инструмент одной из самых признанных бесплатных программ. В качестве альтернативы вы также можете генерировать PDF онлайн. Прочтите ниже как это работает.

PDF24 Creator – бесплатный PDF генератор

PDF24 Creator, это бесплатный PDF генератор для Windows. Генерация PDF файлов реализована с помощью PDF принтера. После установки в вашей Windows появится новый виртуальный PDF принтер. Просто печатайте через этот принтер все, что хотите превратить в PDF. PDF принтер, это единственный путь создания PDF. Программа включает в себя широкий диапазон возможностей, которые помогут вам в генерации PDF. Эта программа бесплатна. Вы можете скачать свежую версию с этого сайта.

PDF24 Онлайн PDF Генератор – Генерация PDF файлов онлайн

Онлайн PDF Генератор, это полезный инструмент, если вы хотите преобразовать документы в PDF формат. Просто выберите документ или отправьте документ по почте в Онлайн PDF Генератор и через несколько секунд ваш PDF файл будет доступен для скачивания или отправлен вам обратно.

Email интерфейс для этого PDF генератора также доступен. Нужно отправить файлы генератору и дождаться ответного письма, в котором будут файлы, сконвертированные в PDF

Множество других PDF генераторов в инструментарии PDF24

Онлайн утилиты от PDF24 решают многие проблемы, связанные с PDF, достаточно просто, быстро и бесплатно. Многие из этих инструментов генерируют PDF файлы. Вы ещё не знакомы с онлайн утилитами PDF24? Взгляните, вы сможете использовать все эти инструменты.

PDF (portable document format) - это популярный формат документов, который не зависит от операционной системы или железа. Очевидно, его можно использовать для генерации отчетов, дипломов, чеков в приложениях, а так же для редактирования документов. Для этих целей лучше всего подойдет java-библиотека iText . Кстати, библиотека доступна так же и для C#.

В статье мы рассмотрим следующие вопросы:

  • Генерация нового PDF документа на примере чека.
  • Заполнение шаблона PDF документа.
  • Краткий обзор некоторых классов и методов библиотеки iText.

Для нетерпеливых сразу же привожу java код, который создает pdf-шаблон чека, а потом заполняет его данными из объекта Receipt. Не забудьте скачать jar-файл библиотеки и положить его на classpath.

Import com.itextpdf.text.*; import com.itextpdf.text.pdf.*; import java.io.FileInputStream; import java.io.FileOutputStream; import java.net.URL; import java.text.NumberFormat; import java.util.Date; public class PdfGeneration { static int FONT_SIZE_SMALL = 16; static int FONT_SIZE_BIG = 32; static int OFFSET = 40; public static void main(String args) throws Exception { createTemplate(); Receipt receipt = new Receipt("This is a veeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeeeeeeeery long purpose " + "text, so it will overflow with font size = 16", 123.45, "Name Surname"); fillInReceipt(receipt); } public static void createTemplate() throws Exception { Document document = new Document(); Font font1 = new Font(Font.FontFamily.HELVETICA, FONT_SIZE_BIG, Font.BOLD); Font font2 = new Font(Font.FontFamily.HELVETICA, FONT_SIZE_SMALL, Font.ITALIC | Font.UNDERLINE); PdfWriter.getInstance(document, new FileOutputStream("template.pdf")); document.open(); // отцентрированный параграф Paragraph title = new Paragraph("Receipt", font1); title.setAlignment(Element.ALIGN_CENTER); title.setSpacingAfter(FONT_SIZE_BIG); document.add(title); // параграф с текстом Paragraph purpose = new Paragraph("Purpose", font2); purpose.setSpacingAfter(FONT_SIZE_BIG); document.add(purpose); // параграф с добавленным чанком текста Paragraph amount = new Paragraph(); amount.setFont(font2); amount.setSpacingAfter(8); amount.add(new Chunk("Amount")); document.add(amount); // параграф с фразой, в которую добавлен чанк Paragraph date = new Paragraph(); date.setFont(font2); Phrase phrase = new Phrase(); phrase.add(new Chunk("Date")); date.add(phrase); document.add(date); document.add(new Paragraph("Name", font2)); Paragraph footer = new Paragraph("Important - please retain for your records - "); // ссылка Anchor anchor = new Anchor("Javenue"); anchor.setReference("http://www..add(anchor); footer.setAlignment(Element.ALIGN_CENTER); footer.setSpacingBefore(FONT_SIZE_BIG); document.add(footer); // картинка, загруженная по URL String imageUrl = "http://www..png"; // Image.getInstance("sample.png") Image stamp = Image.getInstance(new URL(imageUrl)); stamp.setAlignment(Element.ALIGN_RIGHT); document.add(stamp); document.close(); } public static void fillInReceipt(Receipt receipt) throws Exception { PdfReader reader = new PdfReader(new FileInputStream("template.pdf")); PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("receipt.pdf")); PdfContentByte stream = stamper.getOverContent(1); stream.beginText(); stream.setColorFill(BaseColor.BLUE); BaseFont font = BaseFont.createFont(); float pageWidth = reader.getPageSize(1).getWidth(); stream.setFontAndSize(font, FONT_SIZE_SMALL); float v = stream.getEffectiveStringWidth(receipt.getPurpose(), false); float fitSize = (pageWidth-OFFSET*2) * FONT_SIZE_SMALL/v; stream.setFontAndSize(font, fitSize); stream.setTextMatrix(OFFSET, 680); stream.showText(receipt.getPurpose()); stream.setFontAndSize(font, FONT_SIZE_SMALL); String amount = NumberFormat.getCurrencyInstance() .format(receipt.getAmount()); v = stream.getEffectiveStringWidth(amount, false); stream.setTextMatrix(pageWidth - v - OFFSET, 655); stream.showText(amount); v = stream.getEffectiveStringWidth(receipt.getDate() + "", false); stream.setTextMatrix(pageWidth - v - OFFSET, 630); stream.showText(receipt.getDate() + ""); v = stream.getEffectiveStringWidth(receipt.getName(), false); stream.setTextMatrix(pageWidth - v - OFFSET, 605); stream.showText(receipt.getName()); stream.endText(); stamper.setFullCompression(); stamper.close(); } static class Receipt { private String purpose; private double amount; private Date date = new Date(); private String name; public Receipt(String purpose, double amount, String name) { this.purpose = purpose; this.amount = amount; this.name = name; } public String getPurpose() { return purpose;} public void setPurpose(String purpose) { this.purpose = purpose; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public String getName() { return name; } public void setName(String name) { this.name = name; } } }

Для начала необходимо создать экземпляр com.itextpdf.text.Document и с помощью PDFWriter связать его с файлом на файловой системе. Создать шрифт можно с помощью класса Font, указывая размер шрифта, его семейство и стиль.

Основные объекты, с которыми вы будете работать, это Phrase, Paragraph, возможно Anchor и самый маленький кусок текста - Chunk. Обратите внимание, что для каждого параграфа вы можете задать свой шрифт и далее добавлять в параграф объекты Chunk, Phrase, Anchor.

C помощью следующего кода можно добавить картинку в PDF документ:

String imageUrl = "http://site.com/image.png"; Image stamp = Image.getInstance(new URL(imageUrl)); stamp.setAlignment(Element.ALIGN_RIGHT); document.add(stamp);

Не забудьте вызвать document.close() после добавления всех объектов. Это необходимо для того, чтобы корректно закрыть PDFWriter.

Писать поверх существующего PDF документа можно через PdfContentByte. Немного придется помучиться с расположением текста на чеке, но кто говорил, что будет легко. Думаю, код метода fillInReceipt понятен и не требует дополнительных объяснений.

А вот еще классы из iText API, которые могут вам пригодиться:

  • List и ListItem - это список и элемент списка, который может быть добавлен как list.add(new ListItem("item")). Списки как и в HTML могут быть упорядоченные и неупорядоченные.
  • Chapter и Section - раздел и соответственно его секция. Секции можно добавлять с помощью вызова chapter.addSection
  • Более сложный объект PdfPTable, состоящий из ячеек PdfPCell. Можно делать достаточно красивые таблички, но как по мне API не самый удобный.

Вот и все. Если есть вопросы - обращайтесь: всегда рад помочь.

Впервые столкнувшись с задачей генерации документов PDF на сайте любой программист (надеюсь) начнет свой путь с поиска готовых решений, а именно библиотеки, которая ему эти возможности предоставит. Мне нужна была библиотека на PHP, чтобы не мудрить в дальнейшем, т. к. сайт работал на PHP. Хотелось бы еще чтобы она была бесплатной и позволяла свободно пользоваться ей в коммерческих целях. Нашел информацию по 3-м вариантам:

PDFLib в бесплатной версии не поддерживает utf-8, что плохо. С FPDF почти та же история, поэтому эти варианты я отбросил сразу и не стал с ними заморачиваться. Информации по TCPDF в сети достаточно много. Эта библиотека так же входит в состав многих популярных CMS.

Существуют версии для PHP4 и PHP5. Мне нужна пятерка.

Распакуем архив. В каталоге tcpdf увидим следующий список файлов и каталогов:

Cache config doc - документация (можно выкинуть для облегчения) examples - тоже можно выкинуть fonts images 2dbarcodes.php barcodes.php htmlcolors.php tcpdf.php unicode_data.php tcpdf.crt tcpdf.fdf CHANGELOG.TXT LICENSE.TXT README.TXT

TCPDF при генерации документа включает используемые шрифты в документ. Эти шрифты лежат в каталоге fonts. Каждому шрифту соответствуют три файла:

Имя_шрифта.php имя_шрифта.z имя_шрифта.ctg.z

Кириллических шрифтов там нет, что нас, понятно, не устраивает. Если планируете использовать в документе кириллицу, можете удалить все кроме helvetica.php, т. к. он используется библиотекой по умолчанию, и папки utils, где находиться все необходимое для подготовки кириллических шрифтов.

Допустим нам понабился шрифт Arial нормального начертания. Берем шрифт Arial (начертание - normal) из системной папки Windows (например) – файл arial.ttf – и копируем его в папку fonts/utils.

В этой папке есть утилита ttf2ufm, которой мы должны скормить файл arial.ttf:

Php -q makefont.php arial.ttf arial.ufm

Если на вашей локальной машине нет интерпретатора PHP, можете выполнить на сервере файл следующего содержания:

Если вы «скормили» несколько файлов и хотите сформировать сразу все шрифты (для которых созданы файлы *.ufm), имеющиеся в папке, запустите makeallttffonts.php.

После работы данной утилиты у нас в папке utils появятся три файла: arial.php, arial.z, arial.ctg.z, которые мы должны будем переместить в папку fonts. Файлы arial.ttf и arial.ufm нам больше не нужны, поэтому можем смело их удалять.

В итоге мы подготовили шрифт Arial, который будет присоединен к формируемому PDF-файлу. Но при этом нужно учесть, что размер формируемого PDF-документа возрастет примерно на размер файла шрифта arial.z. Также необходимо учесть, что если мы хотим вывести надпись курсивно, нам нужно будет для этого создавать отдельный шрифт ariali, или если полужирным начертанием – arialb.

Пришло время создать наш документ.

// create new PDF document $pdf = new TCPDF("P", "mm", "A4", true, "UTF-8", false);
// set document information $pdf->SetAuthor("Ваше имя"); $pdf->SetTitle("Название нашего документа");
// set default header data $pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, PDF_HEADER_TITLE, PDF_HEADER_STRING);
// set header and footer fonts $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, "", PDF_FONT_SIZE_MAIN)); $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, "", PDF_FONT_SIZE_DATA));
// set default monospaced font $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
//set margins $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); $pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
//set auto page breaks $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
// set font $pdf->SetFont("times", "BI", 16);
// add a page $pdf->AddPage();
// print a line using Cell() $pdf->Cell(0, 12, "Пример 1", 1, 1, "C");
//Close and output PDF document $pdf->Output("example_1.pdf", "I"); ?>

Это очень простой пример. В папке examples найдется много более сложных примеров, иллюстрирующих почти все возможности библиотеки.

Приведу так же несколько полезных ссылок:

    Документация по TCPDF в составе Joomla http://www.tecnick.com/public/code/cp_dpage.php?aiocp_dp=tcpdf_docs

    Статья, которая мне очень помогла в свое время и материалами которой я пользовался при написании данной статьи

Генерация pdf-документов является повседневной задачей в веб-разработке. В перечень таких документов входят счета, накладные, полисы и прочие. Существует множество готовых библиотек для решения этой задачи, в том числе и для php. Например, mpdf ,tcpdf , и многие другие. Файл можно собрать с помощью api этих библиотек, но это довольно долгое занятие. А времени на реализацию задачи много не бывает, не так ли? Поэтому чаще всего pdf-файл создается из html-представления, что довольно удобно. Но, к сожалению, не все так просто. У такого подхода есть множество подводных камней, способных вывести из себя кого угодно.

Например:

  • Стили нельзя подключить отдельно, следовательно они должны быть включены в html-документ отдельным блоком, либо инлайново для каждого элемента. В этом нет ничего страшного, небольшое неудобство.
  • К сожалению, в таких библиотеках некоторые стили могут работать не так как этого от них ожидаешь, либо не работать в принципе. Это самый главный недостаток.
  • Из предыдущего пункта следует, что создать документ максимально соответствующий требованиям очень трудоемко, а порой просто невозможно.
  • А в случае разработки под Битрикс есть еще одна проблемка. Все знают, что для работы платформы требуется в php.ini установить параметр mbstring.func_overload в значение 2. А для создания pdf-файла, содержащего кириллицу потребуется значение 0. Обычно эта проблема решается с помощью настройки веб-сервера, но все равно неприятно.

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

Пришло время перейти к главной части этой статьи. Помимо вариантов предложенных выше, существует альтернативный - PhantomJS .

PhantomJS - это сборка движка WebKit без графического интерфейса, позволяющая в режиме консоли загружать веб-страницу, выполнять JavaScript, полноценно работать с DOM, Canvas и SVG.

Конечно, помимо перечисленных выше возможностей, он дает возможность создавать pdf-файлы.

Каким образом? Говоря простым языком, он загружает требуемую веб-страницу и дает возможность сохранить результат как pdf-файл.

Процесс установки PhantomJS достаточно подробно описан в документации , поэтому я не буду останавливаться на этом вопросе.

Важным моментом в работе PhantomJS является js-файл (далее config.js), который своим содержанием определяет то, что именно мы хотим сделать. На официальном сайте есть множество готовых примеров таких файлов, с помощью которых решаются самые разные задачи, например, unit-тестирование. В числе примеров есть сохранение веб-страницы в формате pdf . Это именно то, что нам нужно.

Пара небольших правок позволит использовать этот пример в наших целях.

var page = require ("webpage" ). create (), system = require ("system" ), address , output , size ; //если аргументов мало или слишком много, выводится сообщение с помощью по использованию if (system . args . length < 3 || system . args . length > 5 ) { console . log ("Usage: config.js URL filename " ); console . log (" paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"" ); console . log (" image (png/jpg output) examples: "1920px" entire page, window width 1920px" ); console . log (" "800px*600px" window, clipped to 800x600" ); phantom . exit (1 ); } else { // обработка аргументов address = system . args [ 1 ]; output = system . args [ 2 ]; page . viewportSize = { width : 800 , height : 800 }; if (system . args . length > 3 && system . args [ 2 ]. substr (- 4 ) === ".pdf" ) { size = system . args [ 3 ]. split ("*" ); page . paperSize = (size . length === 2 ) ? { width : size [ 0 ], height : size [ 1 ], margin : "0px" } : { format : system . args [ 3 ], orientation : "portrait" , margin : "1cm" }; } else { console . log ("Invalid path to pdf!" ); phantom . exit (1 ); } if (system . args . length > 4 ) { page . zoomFactor = system . args [ 4 ]; } // открытие страницы и сохранение результата page . open (address , function (status ) { if (status !== "success" ) { console . log ("Unable to load the address!" ); phantom . exit (1 ); } else { window . setTimeout (function () { page . render (output ); phantom . exit (); }, 200 ); } }); }

$ phantomjs path/to/config.js "url" path/to/pdf/file "A4"

Разберем аргументы по порядку:

  1. Путь до config.js.
  2. Адрес веб-страницы, которую надо преобразовать в pfd-файл.
  3. Путь до pdf-файла, в который произойдет сохранение результата.
  4. Формат pdf-файла.

Получаем искомый pdf-файл.

Возвращаясь к php, это решение довольно просто интегрировать в код.

В простейшем случае это выглядит вот так:

$command = sprintf( "phantomjs %s %s %s %s", $fullPathToConfigJS, $url, $fullPathToSave, $format ); exec($command);

Итак, подведу итоги. На мой взгляд, такое решение имеет следующие достоинства:

  • Сверстать макет для такого документа намного проще. Это очень важно, т.к. не только упрощает разработку, но и не превращает в кошмар последующие правки документа.
  • Избавляет от нужды извращаться с mbstring.func_overload. Но это, несомненно, проблема характерная в основном для Битрикс.

Конечно, есть и недостатки:

  • Не 100% поддержка всех css-стилей. Но в сравнении с библиотеками, перечисленными в начале стати, все очень хорошо.
  • Такое решение может отпугнуть начинающего разработчика.
03.12.2015