- PVSM.RU - https://www.pvsm.ru -

Недавно в одном стартапе я решал задачу генерации билетов в формате PDF. На тот момент уже был готов сайт с устоявшимся стеком технологий, поэтому я искал подход, который бы не потребовал использования дополнительных инструментов. В итоге я предложил сперва создавать билеты в формате HTML, а затем конвертировать в PDF с помощью браузера Chrome. Как оказалось, данным способом можно генерировать не только билеты, богато декорированные CSS, но и самые разные отчеты с графиками на JavaScript. В этой статье я расскажу о том, как для этих целей запустить Chrome, дам несколько советов по настройке CSS, а так же обсужу недостатки данного решения.
Здесь не будут обсуждаться альтернативные варианты, потому как по ним написано уже достаточно, их легко найти, и они представляют собой готовые инструменты, информацию по которым лучше смотреть в первоисточниках — в документациях на официальных сайтах. Предлагаемый способ не является самостоятельным инструментом и больше похож на побочный продукт развития нескольких технологий. В русскоязычном сегменте интернета собранной воедино информации по нему немного, поэтому я и решил восполнить пробел.
Самым главным преимуществом является то, что для генерации PDF браузером Chrome не нужно расширять технологический стек. Фронтенд разработчики создают HTML привычными средствами разработки и сразу видят промежуточные результаты труда в браузере. В это же время Chrome уже наверняка крутится в тестах и перенести его на бекенд не составляет большого труда. Так же следует отметить тот факт, что верстальщику становится доступен весь арсенал css свойств включая Flexbox и Grid.
О недостатках и способах их обхода я расскажу по ходу статьи.
В командной строке вызываем Chrome в безголовом режиме с сохранением страницы в pdf:
chrome --headless --disable-gpu --print-to-pdf https://google.com
Пользователям Linux может понадобиться вместо chrome запускать chromium-browser.
Пользователям MAC может быть полезно предварительно создать alias:
alias chrome="/Applications/Google\ \Chrome.app/Contents/MacOS/Google\ \Chrome"
Если у Вас уже есть генератор HTML документов, вместо https://google.com укажите URL для получения этого документа.
Открываем в локальной директории файл output.pdf и смотрим результат.
Первое, что может броситься в глаза — это наличие Header с датой печати и Footer с URL и нумерацией страниц. Для того, чтобы их убрать нужно добавить несколько CSS правил. Эти правила вряд ли получится добавить на страницу google.com, поэтому для дальнейшей работы лучше создать собственный HTML документ.
В CSS есть специальный медиазапрос @page, который применяется для печати, зададим в нем нулевые отступы так, чтобы Header и Footer просто не помещались:
@page {
size: A4;
margin: 0mm;
}
Этот способ сработает только для одностраничных документов, при печати двух и более страниц на последней внизу останется Footer с URL и нумерацией страниц. Можно явно попросить Chrome отключить отображение Header и Footer, задав параметр печати displayHeaderFooter = False, но на данный момент он не вынесен в интерфейс командной строки. Чтобы добраться до него, понадобятся инструменты для автоматизации работы с браузером: Selenium или puppeteer. Дальше я рассмотрю первый вариант, потому как в моем проекте использовался Python.
Итак, устанавливаем Selenium командой pip install selenium, скачиваем с http://chromedriver.chromium.org/ [1] хромдрайвер, соответствующий Вашей версии Chrome и используем функцию get_pdf_from_html из примера ниже:
import sys
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import json, base64
def get_pdf_from_html(path, chromedriver='./chromedriver', print_options = {}):
# запускаем Chrome
webdriver_options = Options()
webdriver_options.add_argument('--headless')
webdriver_options.add_argument('--disable-gpu')
driver = webdriver.Chrome(chromedriver, options=webdriver_options)
# открываем заданный url
driver.get(path)
# задаем параметры печати
calculated_print_options = {
'landscape': False,
'displayHeaderFooter': False,
'printBackground': True,
'preferCSSPageSize': True,
}
calculated_print_options.update(print_options)
# запускаем печать в pdf файл
result = send_devtools(driver, "Page.printToPDF", calculated_print_options)
driver.quit()
# ответ приходит в base64 - декодируем
return base64.b64decode(result['data'])
def send_devtools(driver, cmd, params={}):
resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
url = driver.command_executor._url + resource
body = json.dumps({'cmd': cmd, 'params': params})
response = driver.command_executor._request('POST', url, body)
if response['status']:
raise Exception(response.get('value'))
return response.get('value')
if __name__ == "__main__":
if len(sys.argv) != 3:
print ("usage: converter.py <html_page_sourse> <filename_to_save>")
exit()
result = get_pdf_from_html(sys.argv[1])
with open(sys.argv[2], 'wb') as file:
file.write(result)
Для получения PDF файла можно запустить этот пример из командной строки указав url и имя файла для сохранения PDF, либо вызвать функцию get_pdf_from_html и передать ей три аргумента:
Следует отметить, что Selenium не имеет стандартного интерфейса для печати страницы в PDF, к тому же это умеет делать только Chrome, поэтому приходится напрямую вызывать driver.command_executor._request.
Теперь разберемся, какие средства доступны для контроля размещения контента на многостраничных документах.
При двусторонней печати можно задать разные отступы от края для правых и левых страниц по отдельности если в дальнейшем предполагается брошюровка:
@page :left {
margin-left: 4cm;
margin-right: 2cm;
}
@page :right {
margin-left: 4cm;
margin-right: 2cm;
}
Для первой страницы можно задать собственное оформление, например, увеличенный отступ от верхнего края:
@page :first {
margin-top: 10cm /* Top margin on first page 10cm */
}
Есть возможность установить разрыв страницы перед заголовком первого уровня так, чтобы он начинался на нечетной странице:
h1 { page-break-before : right }
Посредством свойства page-break-after можно запретить разрыв страницы сразу после некоторого элемента, например, заголовка второго уровня:
h2 { page-break-after : avoid }
Свойство page-break-inside поможет избежать разрыва страниц там, где делать это нежелательно, например посреди таблицы
table { page-break-inside : avoid }
Свойства orphans и orphans помогут избежать разрыва страниц в начале и в конце абзаца:
@page {
orphans:4;
widows:2;
}
На Core i5-8600K 3600MHz в один поток одно преобразование простого документа выполняется за 0.6 сек. На моей портативной печатной машинке конца 2013 года 2.4 Ггц — 1.5 секунды.
Очевидно, что основные ресурсы тратятся на запуск браузера. Можно сократить время преобразования большого количества файлов, если запустить Chrome один раз как микросервис и отправлять ему URL для преобразования. Реализация этого способа выходит за рамки данной статьи.
Я вижу две основные проблемы:
Выводы о допустимости использования такого подхода предлагаю сделать самостоятельно. Каждый проект уникален по своему. Подойдет ли этот способ в Вашем проекте, решать Вам.
Автор: Max_vst
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/326998
Ссылки в тексте:
[1] http://chromedriver.chromium.org/: http://chromedriver.chromium.org/
[2] Источник: https://habr.com/ru/post/459112/?utm_campaign=459112&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.