Шаблонизация PDF

    imageХабрахабр, уважаемые коллеги!

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

    1. PDF формат хорош тем, что он не редактируемый. Во всяком случае рядовой пользователь вряд-ли будет заниматься внесением правок в документ PDF. И значит формат PDF хорошо подходит для обмена важными документами.

    2. PDF формат плох тем, что он нередактируемый ) Т.к. шаблонизация, заполнение набором данных бланка документа PDF в автоматическом режиме затруднена, а в ручном режиме требуется установка платных, тяжеловесных приложений.

    Меня, как программиста, беспокоит прежде всего 2-й пункт. Как в программном приложении впечатать необходимый набор данных в документ PDF?

    Область применения (постановка задачи)


    Сразу хочу обозначить область применения, рамки поставленной задачи, чтобы исключить недоразумения в комментариях:
    1. У вас есть веб API приложение на python с множеством функционала.
    2. Есть бланк документа в формате pdf, в лучшем случае исходный docx файл из которого сделан этот pdf.
    3. Есть требование от бизнес-заказчиков заполнить указанный pdf бланк данными клиента и в формате pdf выдать в браузер (или отправить на почту) клиенту.

    Очередное гугление на эту тему не принесло результатов.
    Удалось нагуглить только, что с впечатыванием всё плохо (Почему так сложно извлекать текст из PDF?, PDF с точки зрения программиста) и есть вариант шаблонизировать сначала docx файл, это сделать не сложно (Заполняем документы в Microsoft Word...), а затем преобразовать в консольном libreoffice (librewrite) docx-файл в PDF. Это всё можно сделать автоматически, из приложения.

    Но во-первых, такое решение означает, что проект будет иметь тяжёлую зависимость от libreoffice.

    А во-вторых, при преобразовании docx в PDF в libreoffice вид документа получается немного не таким, как он смотрится в word, и/или PDF сгенерированном в word из docx файла.

    Перейдём наконец к сути рассматриваемого решения. Конечно «шаблонизация» в данном случае слово громкое, но предлагаемое решение вполне годное и полезное.

    На python (и на php) есть несколько библиотек (не сложно загуглить), которые позволяют впечатывать строки и картинки в PDF-файлы, мы используем pdfrw + reportlab.Canvas. Т.е., в принципе впечатать данные нет проблем, проблема у этих библиотек в том, что для каждого поля нужно задать свои точные координаты в документе, а это значит, что

    1. Нужен какой-то унифицированный функционал, который хранил бы координаты полей не внутри исходного кода, а в отдельном файле. Сразу уточню, что по опыту рекомендую хранить эти координаты в файлах и под контролем версий, т.е. коммитить координаты вместе с соответствующими PDF-бланками и методами, генерирующими тот или иной комплект документов. И не засовывать эти координаты в базу данных, т.к. это затруднит откат к предыдущим версиям (координатам) документов, если возникнет такая необходимость. Тут вроде бы всё понятно.

    2. Эти координаты надо как-то вычислить, а это грустное занятие, если делать это вручную.

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

    Способ применения


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

    1. Скачиваем с гита исходники

    2. Устанавливаем зависимости

    3. Читаем README.md (устанавливаем и настраиваем nginx для статических файлов)

    4. В папке documents создаём подпапку с именем документа, который нужно генерировать и внутри этой подпапки создаём два файла и (если необходимо) одну дирректорию с картинками:

    — form.pdf # бланк документа в который надо впечатывать данные
    — fields.json # параметры полей, которые необходимо впечатывать
    — images # не обязательно, набор картинок, которые необходимо впечатать
    Рекомендую также сохранить исходный docx-файл (если имеется), который не участвует в генерации документа, но пригодится при необходимости внести изменения и перегенерировать бланк документа PDF
    — form.docx # не обязательно, имя любое

    Файл fields.json имеет следующую структуру, например:

    {
        "0": [
            [32.25, 710.25, "fio", "DejaVuSans", 12, 420],
            [425.25, 681.75, "gender", "DejaVuSans", 12, 18],
            [206.25, 681.75, "birth_date", "DejaVuSans", 12, 173],
            [462.75, 681.53, "foto.jpg", "DejaVuSans", 12, 92],
            [146.25, 665.25, "birth_place", "DejaVuSans", 12, 418],
            [228.0, 634.5, "registration", "DejaVuSans", 12, 340]
        ],
        "1": [
            [132.0, 720.76, "1_work", "DejaVuSans", 10, 260],
            [132.0, 697.51, "2_work", "DejaVuSans", 10, 260],
            [132.0, 673.51, "3_work", "DejaVuSans", 10, 141]
        ]
    }
    

    Добавление/удаление строк в этот файл добавляет/удаляет поля, впечатываемые в бланк

    5. Открываем страницу для настроек полей (http://127.0.0.1/tpdf/positioning?pdf_name=ZayavlenieNaZagranpasport&page_num=1)

    6. Настраиваем положение полей с помощью мышки в браузере и сохраняем это положение

    7. Мышкой не всегда точно удаётся установить нужное положение полей, чтобы подровнять положение полей можно открыть файл fieldd.json и поправить координаты вручную. Данные в файле упорядочены по координате Y и каждое поле хранится в своей строке файла. Т.е. файл с координатами полей отформатирован аккуратно, что позволяет вручную, легко вносить необходимые корректировки.

    8. Создаём ещё один метод для печати данного типа документа (если нужно как-то подготовить исходные данные и/или взять их не из фронта, а из бэкэнда).

    9. Если всё в порядке, то коммитим получившийся набор данных fields.json и файлы (только не ко мне на гит, а в свой локальный гит, хотя, если документ может кому-то ещё пригодиться, то можно и публичный банк документов собрать, это идея).

    Полученный файл с координатами можно использовать в другом проекте, на другом языке программирования, например php, ведь координаты в файле записаны в единицах измерения (поинты) которые используются в PDF-файлах.

    Если у вас проект на python, то исходники данного приложения можно просто внедрить в проект и через использование основного класса Tpdf генерировать PDF в любом удобном месте кода.

    Часто нужно сгенерировать не просто один документ из нескольких страниц, а собрать в один PDF-файл несколько документов, каждый из которых должен быть напечатан в нужном порядке и некоторые из них более одного раза. В основном классе данного приложения имеется для этих нужд специальный метод, который генерирует комплект документов, смотрите обработку метод /tpdf/example/.

    Данные в основной класс нужно передавать при его инстансцинировании. Основной класс можно расширять свойствами (@property), которые будут вычисляться на основе входных данных и вставляться в PDF по имени свойства = имени поля. Так в примере выводится поле fio, а данные передаются last_name, first_name, middle_name

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

    Вместо сотни слов, иногда лучше посмотреть видео инструкцию (звук не записывал).


    Опыт реализации (грабли).


    1. Сначала я реализовал это небольшое приложение на библиотеке PyPDF2, но комплект документов из 28 страниц генерировался 3 секунды, как-то долго. Тогда, чтобы ускорить генерацию документов, я решил попробовать мультипоточность, выделив генерацию каждой страницы в отдельный поток, однако это усложнило код но, на удивление, не дало выигрыша производительности, плюс возникли дополнительные ошибки видимо из-за конфликтов процессов. Тогда я попробовал многопроцессность, однако результат оказался тот же — производительность не выросла а в некоторых конфигурациях кода даже ухудшалась. Наконец я решил проверить быстродействие другой, аналогичной библиотеки pdfrw под которую, оказалось, почти не пришлось переписывать код и она заработала почти на порядок быстрее без всякой мультипоточности и мультипроцессности. Т.е. комплект документов из 28 страниц сгенерировался за 0.3 секунды. Не зависимо от библиотеки код изначально оптимизировал с точки зрения повторной генерации страниц: каждая страница заполняется данными один раз и хранится в памяти, и если она должна быть напечатана несколько раз, то первый раз она генерируется, а последующие разы берётся готовая из памяти.
    2. Листание страниц лучше делать не на ajax, так как, чтобы подтянулись новые поля всё равно нужно перезагружать всю страницу.
    3. Было много возни с преобразованием координат с пикселей фронта в поинты PDF. В итоге, опытным путём и путём гугления выяснилось, что отношение фронтовые координаты нужно умножать на 3/4, чтобы получить координаты документа PDF. Обратное преобразование, соответственно, наоборот.

    Нужно сделать (TODO)


    1. Добавление и удаление новых полей с фронта.

      Сейчас, чтобы добавить/удалить поле необходимо добавить/удалить строку в/из файл(а) fields.json
    2. Точное позиционирование полей на фронте
    3. Разобраться с шрифтами, сейчас доступен всего один шрифт, поддерживающий русский алфавит.
    4. Общий метод, принимающий на вход набор данных и возвращающий PDF-документ.
    5. Общий метод, принимающий на вход набор данных и возвращающий комплект документов.
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 35

      0
        0
        Спасибо за комментарий, дополнил статью абзацем об области применения (постановке задачи).
        Область применения (постановка задачи)

        Сразу хочу обозначить область применения, рамки поставленной задачи, чтобы исключить недоразумения в комментариях:
        1. У вас есть веб API приложение на python с множеством функционала.
        2. Есть бланк документа в формате pdf, в лучшем случае исходный docx файл из которого сделан этот pdf.
        3. Есть требование от бизнес-заказчиков заполнить указанный pdf бланк данными клиента и в формате pdf выдать в браузер (или отправить на почту) клиенту.


        Я посмотрел
        Пример файла trml из предложенного вами проекта.
        Похоже, чтобы применить в моём случае эту библиотеку придётся где-то взять сначала обратный преобразователь pdf2trml.
        Мне бизнес даёт pdf-ку, я преобразую её в rtml и потом нет проблем с шаблонизацией.
        Так вот такого преобразователя pdf2trml не существует, может существует docx2trml? Но docx файла в общем случае может не быть.
        Предлагаете преобразовывать pdf в rtml ручками? Это занятие ещё более безнадёжное, чем просто впечатать в pdf данные вручную подобрав координаты впечатывания.
        В общем ваше решение не годится в моём конкретном случае.
          0

          Обратного преобразователя не существует, насколько я знаю. Фарш невозможно провернуть назад.
          docx2trml тоже не видел, но это уже можно руками (XSLT). О точности исходной фирмы речь идти не может, естественно, но содержимое сохранится.
          Касательно именно pdf, то проще подложить исходный PDF как фон trml и сверху уже свои данные. RML такое поддерживает.

            0
            Обратного преобразователя не существует, насколько я знаю. Фарш невозможно провернуть назад.
            О том и речь, что обратного преобразования не существует, поэтому и приходится придумывать подобного рода приложения, как у меня в статье.
            но это уже можно руками (XSLT).
            и
            RML такое поддерживает.
            а RML хотя бы полей впечатываемых где взять? Тоже ручками?
            И то и другое — очень грустное занятие, опять же, чтобы автоматизировать и упростить это «ручками» и предлагаю данный инструмент, про который говорится в статье.
            О точности исходной фирмы речь идти не может, естественно
            Тогда кому это надо?
              0
              > а RML хотя бы полей впечатываемых где взять? Тоже ручками?
              Не понял предложение.
              Если Вам надо заполнять *поля* PDF-форм, то таких либ вагон, и это не про RML и не про генерацию PDF вообще.
              А если натянуть свой PDF поверх чужого — то да, ручками RML или (если религия запрещает пользоваться чужими велосипедами) самопал.
              >> О точности исходной фирмы речь идти не может, естественно
              > Тогда кому это надо?
              Тем, кому не нужен договор, напечатанный с точностью 1 мм.
              Вы уж определитесь в своих желаниях, тогда можно и инструмент подбирать.
        +5
        И значит формат pdf хорошо подходит для обмена юридически значимыми документами.

        Вы как-то неправильно понимаете редактируемость. И юридическую значимость. PDF прекрасно редактируется скажем в Adobe Acrobat, который для этого и предназначен. Конечный пользователь, обладающий акробатом, вполне может внести в документ любые изменения, дописать лишний нолик в сумму платежа, и т.п.

        Для юридической значимости нужно совсем другое — чтобы любое редактирование было заметно другой стороне договора. Чтобы нельзя было дописать нолик так, и чтобы партнер не увидел, что документ редактировался. То есть, что? Правильно, нужна электронная подпись. И редактируйте сколько влезет.

        А что до шаблонов — меня несколько удивляет тот факт, что люди знают про любые наколенные поделки для создания PDF, но при этом вообще не вспоминают про технологию, которая создана ровно для этого — XSL-FO. Ну или Apache FOP, если говорить об инструментах. Т.е. процесс создания шаблона выглядит как-то так:
        — XML документ произвольного формата (в том числе и Word) — это шаблон, сюда подставляются изменяемые поля. Можно Word-ом.
        — трансформация в XSL-FO, где будут колонтитулы, нумерация страниц и вообще все оформление
        — получение PDF при помощи FOP

        Выглядит это возможно и сложно, но на самом деле процесс вполне рутинный.
          +1
          Выглядит это возможно и сложно

          и довольно тяжелое решение.

            0
            Зато работает уже десятки лет. Не, разумеется оно не универсальное, и это лишь решение для генерации своего PDF с нуля, а не редактирования чужого. И для этой же цели у него есть аналоги попроще и полегче, и XSL-FO — это пожалуй самое сложное из возможных. Но оно таки осваивается с нуля за неделю — я проверял пару раз на разных людях.

            Но я подозреваю, что для второй задачи вообще инструмент сделать невозможно — потому что структура чужого PDF может быть любой, и достать оттуда текст реально будет нельзя — потому что его там не будет.
            0

            Или html+css(да даже js) + chromium-headless. Правда, цифровую подпись надо прикручивать отдельно и нет такого обширного функционала макета страниц (но какой-то все-таки есть). Пять лет крутилась, пока не свернули приложение.


            Из старичков помню ещё xslt с шаблонизатором и все это не помню чем в пдф конвертировалось. Но это было давно и не помню уже.

              0
              >chromium-headless
              А оно позволяет вообще задать структуру PDF, хоть как-то? По-моему возможности CSS для печати таки довольно примитивны.

              Но как вариант — почему нет?
                0

                Есть pdfjs от mozillla, посмотрите, что умеет.

                  0
                  Насколько я помню, это парсер, и рендеринг. То есть совсем не для того, чтобы генерировать PDF под шаблоны. Хотя я много лет на него не смотрел.
                  0

                  Точно были хидеры/футеры, принудительный перенос на следующую страницу. Оглавление тогда мы не делали, но… Нашёл такую ссылку https://blog.chromium.org/2020/07/using-chrome-to-generate-more.html?m=1 про accessibility и tagged pdf

                0
                Спасибо за комментарий и извините за задержку ответа, нужно было действительно поизучать вопрос, а времени в обрез.
                Юридическая значимость не суть данной статьи, поэтому, я пожалуй уберу из текста статьи словосочетание «юридически значимыми».
                Но хочу пояснить, что я имел ввиду.
                Статья про электронные подписи:
                В соответствии с ФЗ-63 от 06.04.2011 электронные документы, заверенные ПЭП (простая электронная подпись), не имеют юридической силы. Признаются они равнозначными документам с собственноручной подписью, только если имеется дополнительное соглашение между сторонами.
                Сейчас кредит можно взять по коду из смс (ПЭП) и потом доказывай в суде, что код из смс не имеет юридической силы (шучу, утрирую).
                Ну т.е. юридическая значимость возможна и без проверки редактировался документ или нет, при наличии соглашения между сторонами и ввода кода из смс.
                При этом банки предпочитают документы слать в формате pdf, почему-то. Почему? Наверное потому, что Акробат умеющий редактировать pdf файлы платный и есть далеко не у всех рядовых пользователей, во всяком случае редактирование pdf сложнее чем редактирование docx.
                А если pdf-ка из картинки сделана, то даже акробат не увидит там текста, который можно было бы отредактировать.
                  0
                  Ну, давайте я упрощу — я имел в виду простую вещь, что юридическая значимость и возможность редактирования — не одно и тоже. Что для юридической значимости может быть нужно что-то разное и другое. Проверка на редактирование — ну наверное да, почему бы контрагенту и не поредактировать документ, но с другой стороны — если вы его уже подписали, а он потом вносит правки — то это действие с неочевидными последствиями.
                0

                С точки зрения пользователя проще заюзать какой-нибудь pdffiller и т.п.


                С позиции разработчика, которому нужно решение для задач проекта — сыро.


                Это pet-проект для своих задач или Вы хотите развивать это в продукт?

                  0
                  Дайте ссылку на pdffiller, не знаю что это такое.
                  Дополнил статью абзацем об области применения.
                  В данном случае нужно заполнять pdf данными внутри веб приложения, а не пользователь это ручками делает. Предлагаемое приложение — инструмент веб разработчика.
                  Это pet проект для нужд работы :) На работе это используем, коллеги довольны. Уверен, что это может пригодиться и другим разработчикам, вот и решил поделиться.
                  Я думаю развить это в продукт, если в этом есть потребность у общества, и при наличии времени, но судя по рейтингу статьи — потребности в таком продукте нет.
                    +1
                    pdffiller.com, docusign.com и т.д.

                  0
                  Аналогичную задачу решал намного проще.
                  1. Формировавание PDF было сделано средствами SSRS. Первый лист каждого документа помечался специальным тэгом белым шрифтом на белом поле. Если вдруг печатать надумают.
                  2. При помощи iText (тогда она еще была iTextSharp) и элементарного кода на C#, на основании тегов один PDF в сотню тысяч листов разбивался уже на отдельные документы. Эти документы сразу рассылались по e-mail, который мог содержаться в тэге. Сам тэг, естественно, удалялся. Производительность была вполне приличной. Во-всяком случае, нить парсинга PDF постоянно ожидала нить отсылки по smtp, а не наоборот.
                    0
                    Делал в Delphi (можно и FPC или вообще что угодно где можно зацепиться к OLE) через OLE (установленный офис конечно нужен). Делаем шаблон документа с нужными полями в Word/Excel, заполняем эти поля из кода как надо, далее через OLE же и экспортируем как pdf куда надо. Конвертировал таким образом около 5000 документов одного предприятия и приводил к единообразию, правда не в pdf, а просто другой docx, но никаких проблем не вижу. Можно найти и бесплатные компоненты для создания PDF даже без сторонних библиотек в том числе скорее всего всё это будет работать и в Linux, но это не точно.
                      0
                      С OLE не сталкивался, но если притянуть зависимость Word/Excel, то это не Linux и в любом случае зависимость тяжёлая, т.е. приложение на 10 000 строк кода будет требовать установки целого приложения. Я написал об этом в статье.
                      Какие ещё можно найти бесплатные компоненты? Я не нашёл.
                        0
                        Конкретно для Delphi вроде как есть Synopse PDF Engine и PowerPDF из бесплатных. К сожалению дел с ними не имел, ничего сказать более не могу. Опять же только Win.
                      0
                      А что делать с полями которые могут стать многострочными, скажем юридический адрес или еще что то такое?
                      «ул. Тверская д. 7 к 3. Москва» влезет а вот «ул. Героев Освободителей д. 3 к.3 стр. 4 кв. 7 посёлок городского типа Краснозатонский, городской округ Сыктывкар, Республика Коми» как быть?
                        0
                        Посмотрите видяшку, там же показано, что реализован перенос строк, если текст не помещается в заданную ширину, то он переносится на другую строку. На примере прля 3_work это показано.
                          0
                          Не обратил внимание сразу, но выглядит как то не очень, ячейка по логике должна растягиваться.
                          Выглядит как поиграться, не для боевых задач.
                          Посмотрите связку каких ни будь шаблонизаторов для doc или docx и конвертируйте через openoffice в headless, рабочая схема.
                          Вот боевой пдф с текущего проекта, как тут без переноса строки быть? никак
                          image

                          Как альтернатива генерируйте документ в html и сделайте pdf папетиром
                            0
                            Ячейки можно растягивать и сжимать, посмотрите видяшку.
                            openoffice — рабочее решение, но тяжёлая зависимость, я писал об этом.
                            И второй раз вам говорю, перенос строк реализован уже.

                            На входе бизнес даёт готовый документ, генерировать html из него не имеет смысла, нет времени.
                              0
                              Не увидел как таблица в документе растягивается
                              image

                              Но и логично что в данном случае это не возможно, в примере я кидал документ где таблица растягивается из за переноса строк и двигает остальной контент
                            0
                            Перенос — мелочи.
                            Сложнее, когда нужна таблица, причем не одна. Например, как у меня, при формировании ЕПД на коммунальные услуги (таблица услуг, таблица ОДПУ, таблица расчета ОДН и таблица расчета суммы к оплате). Причем нужно корректно переходить на второй лист, если он потребовался с повторением заголовка разорванной таблицы. И еще персональный QR код в довесок.
                              0
                              Вставить QR код в моём решении нет проблем.
                              Перход со страницы на страницу с сохранением заголовка — похоже вы генерируете pdf с нуля.
                              Если есть готовая pdf-ка, как у меня, то там уже все страницы статические, все заголовки на всех страницах указаны, остаётся только сделать перенос строк на следующую страницу — это вопрос доработки предложенного мной решения.

                              А так, поделитесь своим решением, как вы решаете эти задачи?
                                0
                                Писал же выше.

                                Я генерирую PDF средствами SSRS. Поэтому для форматирования пользуюсь его возможностями, которые весьма широки.

                                А вот как в Вашем решении вставить QR код я не понял. Это же не статическая картинка, а закодированные данные.
                                  0
                                  А… Читал ваше сообщение с вариантом решения, буду иметь его ввиду, если что, правда это не мой стэк на данный момент.

                                  QR код можно в методе API генерировать и сохранять в подпапку images с заданным именем файла, а по имени картинка подтянется уже в pdf. Это как сейчас можно сделать.
                                  Правда это может вызвать коллизии, поэтому нужна доработка (но она не большая), чтобы картинки можно было генерировать динамически и передавать их на вставку в pdf не через диск, а через оперативную память.
                                  Вставить QR код в моём решении нет проблем.
                                  Я имел ввиду, что доработку можно сделать, это не сложно.

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

                          А что, если сверстать в html и отрендерить в pdf?

                            0
                            Есть исходный pdf, сколько у вас займёт времени сверстать его в html?
                            Неделя? Это очень много сил и времени требует. И в итоге pdf-ка не похожая на исходную получится. И за эту неделю бизнес передумает и скажет, что бланк изменился и нужно ещё несколько бланков сделать.
                            Вёрстка в html не подходит.
                            0
                            Встала проблема — накладная в PDF, надо было в него добавить различные данные как ФИО, даты, различные цифры. Все это делается в браузере, файлом php — inkscape превращает pdf в формат SVG (который по сути текст) далее правленный SVG формирует результирующий файл pdf.
                              0
                              Как-то слишком сжато и поэтому не понятно.
                              Как «это делается в браузере»?
                              Каким «файлом php»? Дайте исходник.
                              inkscape — странная и тяжёлая зависимость для простого веб-приложения, как в моём случае.
                              И как SVG формирует pdf? Опять inkscape?

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

                              Самое читаемое