python

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

  • понедельник, 19 апреля 2021 г. в 00:35:39
https://habr.com/ru/post/553082/
  • Python
  • PDF
  • GitHub


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

Проблема впечатывания данных в pdf документ не нова, не я первый и не я последний кто с ней сталкивается, поэтому решил поделиться опытом решения и заодно представить вашему вниманию небольшое веб приложение по этой теме.
1. pdf формат хорош тем, что он не редактируемый. Во всяком случае рядовой пользователь вряд-ли будет заниматься внесением правок в документ pdf. И значит формат pdf хорошо подходит для обмена юридически значимыми документами.
2. pdf формат плох тем, что он не редактируемый :) Т.к. шаблонизация, заполнение набором данных бланка документа pdf в автоматическом режиме затруднена, а в ручном режиме требуется установка платных, тяжеловесных приложений.

Меня, как программиста, беспокоит прежде всего 2-й пункт.
Как в программном приложении впечатать необходимый набор данных в документ 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. Общий метод, принимающий на вход набор данных и возвращающий комплект документов.