Генерация PDF… Эта тема не нова, однако порой можно столкнуться с некоторыми тонкостями, в итоге став на тернистый путь велосипедостроения. Сегодня я расскажу, как разрабатывал один такой велосипед.

Мне понадобилось сделать генерацию отчетов в PDF. По ряду причин я решил сделать это на стороне клиента. Беглый поиск предоставил мне выбор между jsPDF и pdfmake. Остановился на первом. А теперь подробнее…



Для начала хочу сказать, что хотя jsPDF и великолепная штука, документация этого проекта местами невменяемая,
что у человека психически неподготовленного вызывает желание ругаться нецензурным матом. Вспоминается документация Symfony: ее читаешь, а потом идешь гуглить с вопросом «а как?» (либо идешь в исходники).

Первый подводный камень, брошенный в мою сторону этой библиотекой, было отсутствие поддержки русского языка (и UTF-8 в целом, насколько мне удалось выяснить).

(pdfmake напротив — умеет работать с UTF-8, однако от использования этой библиотеки я вскоре отказался.)

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

Пришлось разбивать таблицу скриптом, заносить во временный контейнер, делать из него canvas и по новой. Вот, что получилось в итоге:
app.factory("PDF", function() {
    
    return {
        tableToPDF: function() {
            var pdf = new jsPDF('p', 'pt', 'a4');

            var rows = $("table").find("tr");
            var pdfContainer = $(".tmp-pdf-container");

            var pdfInternals = pdf.internal;
            var pdfPageSize = pdfInternals.pageSize;
            var pdfPageWidth = pdfPageSize.width;
            var pdfPageHeight = pdfPageSize.height;

            var partialSize = 10;
            var contentSize = 0;
            var marginTop = 20;
            var index = 0;

            // создаю новую таблицу каждые partialSize строк
            var generatePartial = function() {
                var partial = "";
                rows.each(function(i, row) {
                    if (i >= index && i <= partialSize) {
                        partial += "<tr>" + $(this).html() + "</tr>";
                        index++;
                        if (index >= partialSize) {
                            partialSize += 10;
                            return false;
                        }
                    }
                });

                return partial;
            }

            var processCanvases = function() {
                if (index >= rows.length) {
                    pdfContainer.html("");
                    //pdf.output("datauri");
                    pdf.save("TEST.pdf");
                    return;
                }

                var partial = generatePartial();

                // generate table with that rows
                var table = $(document.createElement("table"));
                table.append("<tbody>" + partial + "</tbody>");    

                // insert table to temporary div
                pdfContainer.html("<table class='table table-fixed-width table-condensed'>" + table.html() + "</table>");
                // hide unnecessary columns
                pdfContainer.find(".non-printable").css("display", "none");

                // generate canvas from that table
                html2canvas(pdfContainer, {background: "white"}).then(function(canvas) {
                    // contentSize подбирал экспериментально
                    // на формате а4 у меня умещается несколько partial,
                    // поэтому жду, пока дойдет до конца страницы и только затем создаю новую
                    if (contentSize < 2) {
                        contentSize ++;
                        pdf.addImage(canvas, "jpeg", 20, marginTop, pdfPageWidth-40, 0);
                        // формулу и коэффициент подобрал экспериментально
                        // у меня 0.01 работает, тут 0.05, пока не разобрался, как правильно это вычислить
                        marginTop += canvas.height/ (canvas.width / pdfPageHeight + (pdfPageWidth / pdfPageHeight) - 0.05);
                    } else {
                        pdf.addImage(canvas, "jpeg", 20, marginTop, pdfPageWidth-40, 0);
                        // эта проверка нужна, чтобы не создать лишнюю пустую страницу в конце
                        if (index < rows.length) {
                            pdf.addPage();
                        }
                        contentSize = 0;
                        marginTop = 0;
                    }
                    // next iteration
                    processCanvases();
                });
            }

            processCanvases();

        }
    }
});


<div class="tmp-pdf-container" style="position: absolute; left: -9999; width: 1000px"></div>


Отдельно хочу упомянуть формат PNG. Штука хорошая (насколько я знаю, он поддерживается старыми браузерами, в отличие от image/jpeg), но PDF утяжеляет в разы. Вдобавок на больших отчетах браузер гарантированно ляжет, конкретно у меня хром выбрасывал окно ошибки, а ФФ вообще укладывал себя и систему, поэтому спасала только кувалда.

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

также, html2canvas, не умеет генерировать canvas из кода, поэтому нужно создавать какой-нибудь временный элемент. Плюс он не отрисовывает невидимые элементы. Где-то на stackoverflow советовали iframe, лично я добавил div с position:absolute и left:-9999, чтобы не мешал (правда, на планкере не получилось загнать за экран)

Также возникает сложность при нарезке таблицы: колонки разные. Я это вылечил, добавил следующий стиль:
 .table-fixed-width {
    table-layout: fixed;
}


Вот сам пример: plnkr.co/edit/r3BaDwHpK5H9giwpqrBb

Заключение: писать велосипеды — не всегда плохо. Нередко это помогает узнать что-то новое. Однако это не должно становиться привычкой, поэтому всегда нужно читать доки, хотя бы для того, чтобы слегка попсиховать.

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

Проголосовал 51 человек. Воздержалось 40 человек.

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

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


  1. Ag47
    26.02.2016 22:01
    +1

    Чтобы спрятать лучше не left:-9999, а right:100%;bottom:100%


    1. ivanuzzo
      26.02.2016 22:18

      а почему так лучше?


      1. alltiptop
        27.02.2016 00:22
        +1

        потому что ширина может быть больше 9999, да и логически понятнее


  1. Ag47
    26.02.2016 22:02
    +1

    Расскажите подробнее, пожалуйста, как решили проблему русского языка и что не так с pdfmake.


    1. ivanuzzo
      26.02.2016 22:18

      c pdfmake все так, мне просто формат не понравился. Проблему русского языка решил через canvas: т.е. я делаю часть таблицы в виде картинки и затем эту картинку вставляю в страницу. И так, пока таблица не закончится (либо ресурсы компьютера, но я до такого не доходил, сгенерировал pdf на 86 страниц и хватит пока что)


      1. barkalov
        27.02.2016 16:57

        Делать pdf из растровых картинок — это как-то совсем не правильно.


        1. ivanuzzo
          27.02.2016 18:17

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


          1. barkalov
            28.02.2016 03:48

            Всё векторное делать вектором (svg?), всё текстовое — текстом. Как именно в jsPDF это делать я не знаю, если вы об этом.


  1. Akuma
    27.02.2016 11:06

    У jsPDF есть весомый недостаток. Пытался с его помощью сохранять страницу в PDF вроде все круто, но больше одной страницы не делает. А если поставить спец. опцию для мультистраничных документов, то ужасно растягивает все по вертикали.

    А вот про pdfmake не слышал даже, надо будет посмотреть.


  1. and7ey
    27.02.2016 20:43
    +1

    pdfmake очень даже неплох — не понимаю почему автор выбрал jsPDF (а потом изобретал костыли в виде картинок). Если кому нужен живой пример не очень простого pdf-документа, сделанного с помощью pdfmake, — можете посмотреть на моем сайте http://uts-online.ru/ (ссылка на pdf появляется после того, как введете данные и получите результаты расчета).


  1. MTonly
    28.02.2016 00:20
    +2

    Т.?е. на самом деле у вас не настоящий масштабируемый векторно-текстово-выделяемый PDF, а просто растровые изображения с JPEG-артефактами, вставленные в PDF-контейнер?


    1. ivanuzzo
      28.02.2016 00:37

      в принципе, это риторический вопрос. Вы правы.

      Вообще, когда я искал решение, где-то на stackoverflow мелькнуло предложение делать через canvas. Тема была не особо развита и я решил обучения ради довести ее до конца. На хабр выложил для того, чтобы в интернете была какая-то завершенная картина по данному вопросу (тем более, что не я первый искал ответ "как сделать именно так", поэтому пусть начинающие и продолжающие прочтут и увидят недостатки), но также для критики и для поиска альтернатив.

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