Разбираем возможности конвертирования HTML в PDF браузером Google Chrome

в 18:49, , рубрики: chromium, css, Google Chrome, html, PDF, python, selenium

Разбираем возможности конвертирования HTML в PDF браузером Google Chrome - 1

Недавно в одном стартапе я решал задачу генерации билетов в формате 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

В CSS есть специальный медиазапрос @page, который применяется для печати, зададим в нем нулевые отступы так, чтобы Header и Footer просто не помещались:

@page {
    size: A4;
    margin: 0mm;
}

Этот способ сработает только для одностраничных документов, при печати двух и более страниц на последней внизу останется Footer с URL и нумерацией страниц. Можно явно попросить Chrome отключить отображение Header и Footer, задав параметр печати displayHeaderFooter = False, но на данный момент он не вынесен в интерфейс командной строки. Чтобы добраться до него, понадобятся инструменты для автоматизации работы с браузером: Selenium или puppeteer. Дальше я рассмотрю первый вариант, потому как в моем проекте использовался Python.

Запускаем Chrome через Selenium

Итак, устанавливаем 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 и передать ей три аргумента:

  1. path — url html документа;
  2. chromedriver — путь на локальной машине к хромдрайверу (по умолчанию должен лежать в локальной директории);
  3. print_options — дополнительные атрибуты печати.

Следует отметить, что Selenium не имеет стандартного интерфейса для печати страницы в PDF, к тому же это умеет делать только Chrome, поэтому приходится напрямую вызывать driver.command_executor._request.

Теперь разберемся, какие средства доступны для контроля размещения контента на многостраничных документах.

Типографика в CSS

При двусторонней печати можно задать разные отступы от края для правых и левых страниц по отдельности если в дальнейшем предполагается брошюровка:

@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 для преобразования. Реализация этого способа выходит за рамки данной статьи.

Что еще не так?

Я вижу две основные проблемы:

  1. Невозможность простого определения положения элементов в документе. Это делает затруднительным формирование оглавления с автоматическим указанием номеров страниц, особенно, если размер контента заранее неизвестен.
  2. Преобразованием занимается Chrome — продукт Google который собирает о пользователях самую разную информацию. Если утечка данных из документа недопустима, к предлагаемому решению нужно относится осторожно — закрыть браузеру выход на внешние ресурсы, или вовсе поискать другое решение. Использование Chromium с открытыми исходниками не решает проблемы — в нем уже находили жучки от Google.

Выводы

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

Автор: Max_vst

Источник


* - обязательные к заполнению поля