Хабрахабр

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

На тот момент уже был готов сайт с устоявшимся стеком технологий, поэтому я искал подход, который бы не потребовал использования дополнительных инструментов. Недавно в одном стартапе я решал задачу генерации билетов в формате PDF. Как оказалось, данным способом можно генерировать не только билеты, богато декорированные CSS, но и самые разные отчеты с графиками на JavaScript. В итоге я предложил сперва создавать билеты в формате HTML, а затем конвертировать в PDF с помощью браузера Chrome. В этой статье я расскажу о том, как для этих целей запустить Chrome, дам несколько советов по настройке CSS, а так же обсужу недостатки данного решения.

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

Почему выбран именно этот вариант?

Фронтенд разработчики создают HTML привычными средствами разработки и сразу видят промежуточные результаты труда в браузере. Самым главным преимуществом является то, что для генерации PDF браузером Chrome не нужно расширять технологический стек. Так же следует отметить тот факт, что верстальщику становится доступен весь арсенал css свойств включая Flexbox и Grid.
О недостатках и способах их обхода я расскажу по ходу статьи. В это же время Chrome уже наверняка крутится в тестах и перенести его на бекенд не составляет большого труда.

Решаем задачу одной строкой

В командной строке вызываем 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 для получения этого документа.

Для того, чтобы их убрать нужно добавить несколько CSS правил. Открываем в локальной директории файл output.pdf и смотрим результат.
Первое, что может броситься в глаза — это наличие Header с датой печати и Footer с URL и нумерацией страниц. Эти правила вряд ли получится добавить на страницу google.com, поэтому для дальнейшей работы лучше создать собственный HTML документ.

Добавляем CSS

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

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

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

Запускаем 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;
}

Что с производительностью?

6 сек. На Core i5-8600K 3600MHz в один поток одно преобразование простого документа выполняется за 0. 4 Ггц — 1. На моей портативной печатной машинке конца 2013 года 2. Можно сократить время преобразования большого количества файлов, если запустить Chrome один раз как микросервис и отправлять ему URL для преобразования. 5 секунды.
Очевидно, что основные ресурсы тратятся на запуск браузера. Реализация этого способа выходит за рамки данной статьи.

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

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

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

Выводы

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть