Наверняка не очень редко возникает задача печати HTML-документов с какого-то сервера в точности как задумано автором этого сервера. Делать это лучше всего не в надежде на браузер клиента, а на стороне сервера. А если на сервере крутится нечто на питоне (Django/Flask/тысячи их), то хорошо бы оценить во что это обойдется.
Для тестов выбирались такие библиотеки, чтобы как минимум были в виде пакетов в официальных репо RH-based дистрибутивов или же - в крайнем случае - можно было таковые собрать. И чтобы без долгих танцев с бубном.
В macOS всё ставилось с помощью homebrew и pip3, в Fedora - из стандартного репо (искл. xhtml2pdf - этого в репах нет, но при должной усидчивости за пару часов можно собрать вполне себе цивильный rpm).
Дано:
После тщательного отбора кандидатов накопилось аж 3:
python-pdfkit - адаптер к вызову бинарника wkhtmltopdf.
weasyprint - прокладка между html5lib и reportlab.
xhtml2pdf - примерно то же самое, что и weasyprint, но со своими
тараканамиособенностями. В таблице указано как "Pisa" (основной модуль).
Платформ для тестирования набралось под руками тоже 3 (все x64):
MacBook - Apple MacBookPro9.2 (13" mid 2012, i5-3210M (2.5GHz)), HDD, macOS 10.15 "Catalina", Python 3.9 (brew)
LinBook (так это назовем) - тот же самый макбук, но с Fedora 33, Python 3.9
DeskTop - Intel G3450 (3.4GHz), HDD, Fedora 33, Python 3.9
Документов для тестов - 3 (все - на одну страничку каждый):
ПД4 - квитанция на оплату налогов и сборов в Сбер (форма ПД-4сб). HTML ручной работы, максимально соответствующий стандартам. Требования к точности передачи задумки автора в печати довольно высокие.
Инструкция - чей-то документ с заголовком, комментариями, табличками и местом для подписи. Получен из .doc экспортом из Word 2007. HTML не так, чтобы очень тяжелый, но на тяп-ляп. То есть как оно и будет в жизни. Требования к точности - никакие.
Р21001 - последний листик (стр.5Б) формы Р21001 - с якорями для сканера, буквами в квадратиках и всем остальным, что мы так любим в документах для налоговой. Экспорт из Excel 2007, IE6-совместимо. Получилось 2 МБ формально правильного HTML, но совершенно фееричной разметки, то есть достаточно тяжелого для парсера-генератора. Требования к точности очень высокие.
Решение:
Код на коленке
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Benchmark of html-to-pdf converters.
(c) @justhabrauser, GPLv3.
"""
# 1. system
import os
import sys
from time import time
# 2. 3rd
from pdfkit import from_string # https://github.com/JazzCore/python-pdfkit
from weasyprint import HTML # https://github.com/Kozea/WeasyPrint
from xhtml2pdf.pisa import CreatePDF # https://github.com/xhtml2pdf/xhtml2pdf
def __pdfkit(html: str) -> bytes:
return from_string(html, False, options={'quiet': ''})
def __weasy(html: str) -> bytes:
return HTML(string=html).write_pdf()
def __pisa(html: str) -> bytes:
pdf = CreatePDF(html)
if not pdf.err:
pdf.dest.seek(0)
return pdf.dest.read()
def main(indir: str, outdir: str, count: int) -> None:
# 1. Load all htmls
modules = (__pdfkit, __weasy, __pisa)
html_list = list() # (filename, content)[]
dir_list = os.listdir(indir)
dir_list.sort()
for i, fn in enumerate(dir_list):
fpath = os.path.join(indir, fn)
if os.path.isfile(fpath) and fpath.endswith(".html"):
print("Load '{}'".format(fn), file=sys.stderr)
with open(fpath, "rt") as i_f:
html = i_f.read()
html_list.append(html)
# 2. write results (and warm up)
for j, m in enumerate(modules):
with open(os.path.join(outdir, "%d_%d.pdf" % (i, j)), "wb") as o_f:
o_f.write(m(html))
# 2. for C times x I pages x J engines:
print("Count\tPage\tEngine\tTime\n=====\t====\t======\t====")
for c in range(count): # count
for i, h in enumerate(html_list): # html page
for j, m in enumerate(modules): # engine
t0 = time()
m(h)
t1 = time()
print("{}\t{}\t{}\t{}".format(c, i, j, t1-t0))
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Usage: {} <dir_with_htmls> <output_dir_for_pdfs>".format(sys.argv[0]), file=sys.stderr)
elif not os.path.isdir(sys.argv[1]):
print("Input '{}' is not dir or not exists.".format(sys.argv[1]), file=sys.stderr)
elif not os.path.isdir(sys.argv[2]):
print("Output '{}' is not dir or not exists.".format(sys.argv[2]), file=sys.stderr)
else:
main(sys.argv[1], sys.argv[2], 5)
Среднее время обработки каждого документа (в разрезе документов, библиотек и платформ (D=DeskTop, L=LinBook, M=MacBook)), сек.:
Lib | ПД4 | Смета | Р21001 | ||||||
D | L | M | D | L | M | D | L | M | |
Pdfkit | 0,36 | 0,44 | 1,49 | 0,36 | 0,44 | 1,23 | 1,3 | 1,9 | 6,9 |
Weasy | 0,36 | 0,47 | 0,60 | 0,27 | 0,36 | 0,65 | 26,1 | 34,8 | 54,7 |
Pisa | 0,12 | 0,17 | 0,28 | 0,29 | 0,41 | 0,68 | 20,4 | 27,3 | 42,2 |
Выводы: таракан без ног не слышит ©
Общий вывод - счастья нет. То есть я не смог ни одного кандидата однозначно выгнать на мороз или наградить золотой медалью. В среднем по больнице видно, что pdfkit дольше запрягает, но потом быстрее едет, но это и без тестов логично (хотя разница все-равно впечатляет). Ну а так каждый может оценить цифры, протестировать самостоятельно и сделать свои выводы. Я могу только привести свои личные впечатления:
pdfkit. Все-таки это не чистокровный питон и даже не обертка C-либы, что нарушает внутреннюю гармонию и бесит перфекционизм. Радует высокое качество полученного PDF, максимально точная передача задумки (реально WYSIWYG), максимальная скорость на тяжелых документах. Не радует неторопливость на мелких задачах и почти полная неуправляемость.
weasyprint. Бедненько - но чистенько. Всеядное, приемлемая (а иногда и неплохая) скорость, достаточно предсказуемый результат. Но без наворотов и без рекордов.
xhtml2pdf. Вредное. HTML должен не просто идеально соответствовать стандартам, он должен еще понравиться этой либе, иначе "инжалид дежице". Отдельно идут упражнения с кириллицей (кстати, я тестировал без этих упражнений (лениво), то есть не совсем корректно) и фееричность получаемого результата. За это там куча наворотов и в среднем хорошая скорость работы (как для питона).
Отдельно стоят вопросы управления разрывами страниц, нумерация страниц, хорошо бы ещё попробовать iText7 (но это вязать python с java, что из категории секаса переводит вопрос в категорию прона), wkhtmltopdf-static и иные окружения. Но я хотел просто быстро оценить порядок скорости на целевой лично для меня платформе (RHEL8+).
anger32
Очень интересует вопрос оверлеев в pdf. Наложить номера страниц на готовый, добавить хдер-футер. Есть pdftk, но и как-то больше ничего не попадалось на глаза
justhabrauser Автор
Легенды гласят, что это умеет PyPDF2/PyPDF4 и другие библиотеки операций с сырыми PDF (да хоть и тот же reportlab, если сильно прижмет).
Это если речь о питоне, а не яве (pdftk и иже — это к ним).
anger32
Натыкались на PyPDF2, не обнаружили кастомных оверлеев. Правда и искали минут 5-10, так как посчитали что проект умер ввиду отсутствия коммитов несколько лет. А вот PyPDF4 не видели, спасибо.
Попадался еще pyfpdf. Пробовать не доводилось?
medvoodoo
wkhtmltopdf умеет ставить страницы, хедер-футеры и т.п.
Нужно быть внимательным, разные версии немного по-разному рендерят pdf(масштаб на страницу, отступы).
Из подводных камней мы сталкивались не с временем рендеринга, а с тем, что некоторые верстки с js генерятся в документ, который дико долго открывается и тормозит.
anger32
Обратите внимание, я спрашивал о другом. wkhtmltopdf используется для генерации большого числа разрозненных PDF, которые в последующем успешно склеиваются. Так что исходный материал в данном случае — PDF.
Не видел именно у этой тулзы функций работы с готовым PDF.