Разбираем возможности конвертирования HTML в PDF браузером Google Chrome
- четверг, 15 августа 2019 г. в 00:18:41
Недавно в одном стартапе я решал задачу генерации билетов в формате 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"
UPDATE: В комментариях внесли уточнение, что пользователям Windows необходимо явно задавать имя PDF файла --print-to-pdf=output.pdf
Если у Вас уже есть генератор 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/ хромдрайвер, соответствующий Вашей версии 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 для преобразования. Реализация этого способа выходит за рамки данной статьи.
Я вижу две основные проблемы:
Выводы о допустимости использования такого подхода предлагаю сделать самостоятельно. Каждый проект уникален по своему. Подойдет ли этот способ в Вашем проекте, решать Вам.