Создание PDF-файлов всегда было проблемой на протяжении всей моей карьеры разработчика программного обеспечения. Большинство библиотек и реализаций PDF по-прежнему кажутся очень низкоуровневыми, и вы в основном всегда в конечном итоге создаете свой собственный механизм рендеринга.
Создать собственный движок рендеринга?
В качестве примера рассмотрим фрагмент кода, взятый из документации PDFKit:
doc.fillColor('green') .text(lorem.slice(0, 500), { width: 465, continued: true }) .fillColor('red') .text(lorem.slice(500))
Приведенный выше код позволяет вам взять текст, хранящийся в переменной с именем lorem, и отобразить первые 500 символов зеленым цветом, а все последующие - красным. Но не выглядит ли это слишком сложным? Нет, это не так. Хотя в реальных условиях вы, вероятно, предпочтете выделить несколько конкретных слов в блоке текста. Для этого вам нужно будет определить соответствующие диапазоны затронутых символов в этом тексте и динамически построить что-то вроде приведенного выше кода. Теперь представьте, что вы также хотите создать макет столбцов или, возможно, создать таблицу и вставить несколько изображений. Все усложняется довольно быстро, и вы действительно можете создать механизм рендеринга PDF.
Использовать существующий движок рендеринга
Так получилось, что у нас есть очень сложные механизмы рендеринга для сложных макетов, которые называются браузерами. Однако запуск веб-браузера для рендеринга PDF-файла может вызвать много накладных расходов, и это правда. Если ваша цель - создавать PDF-файлы с максимальной эффективностью, не рекомендуется делать это с помощью браузера. Однако в любом другом случае преимущества могут намного перевесить проблемы с производительностью.
Входит кукловод.
Puppeteer - это, по сути, автоматизированный экземпляр Chromium для Node.js. Его можно использовать для многих вещей, таких как автоматическое тестирование пользовательского интерфейса, автоматическая отправка форм и просмотр веб-страниц, а также автоматическое создание снимков экрана и создание PDF-файлов.
Создать PDF-файл с помощью Puppeteer довольно просто:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch() const page = await browser.newPage() await page.goto('https://www.medium.com') await page.pdf({path: 'medium.pdf', format: 'A4'}) await browser.close() })()
Итак, вы в основном запускаете браузер, открываете страницу, распечатываете страницу в файл PDF и закрываете браузер.
Вместо файла вы также можете распечатать свой PDF-файл в буфер, опуская параметр пути:
const buffer = await page.pdf({format: 'A4'})
Создание макета
Теперь все, что осталось сделать, это создать макет с помощью HTML и CSS. Фактически, вы можете использовать любую понравившуюся веб-технологию - даже JavaScript, SVG или Canvas.
Мне нравится максимально контролировать макет отдельных страниц, поэтому я счел очень полезным создать контейнер страницы:
.page { position: relative; overflow: hidden; padding: 0.8in; page-break-after: always; } .page.landscape { width: 11.7in; height: 8.2in; } .page.portrait { width: 8.3in; height: 11.6in; }
Вы можете использовать position: relative
, чтобы иметь возможность абсолютно позиционировать некоторые из ваших элементов на странице.
Вы также можете использовать page-break-after: always
, чтобы принудительно разрывать страницы после каждой страницы.
Затем вы можете просто создать свои PDF-страницы в HTML:
<body> <div id="page1" class="page landscape"> … </div> <div id="page2" class="page landscape"> … </div> </body>
Если вы хотите использовать альбомную ориентацию, как в приведенном выше примере, вам также нужно указать Puppeteer для печати в альбомной ориентации:
page.pdf({format: 'A4', landscape: true})
Вы также можете включить печать фона, если вам нужно использовать цвета фона или изображения:
page.pdf({format: 'A4', landscape: true, printBackground: true})
Фактически, использование фоновых изображений вместо тегов img может дать вам больше контроля при размещении изображений внутри макета:
.page .title-image { width: 100%; height: 4in; background: url("…") no-repeat center center; background-size: contain; } … <div class="page landscape"> <div class="title-image"></div> … </div>
Таким образом, вы можете использовать свойства CSS, такие как background-position и background-size, для оптимального размещения ваших изображений. Это может быть особенно полезно при работе с динамическим контентом, размеры изображения которого могут отличаться.
Благодаря абсолютному позиционированию размещение верхних и нижних колонтитулов также не составляет проблем:
.page .footer { position: absolute; left: 0.8in; right: 0.8in; bottom: 0.2in; border-top: 1px solid #000; padding: 0.1in 0 0; } … <div class="page landscape"> … <div class="footer">…</div> </div>
Вы получаете таблицы, границы, поля, отступы, позиции и цвета бесплатно. Разумеется, форматированный текст также поставляется с использованием CSS для изменения размеров и стилей шрифтов или с использованием таких элементов HTML, как strong или em.
Вы даже можете добавить собственное начертание шрифта, используя веб-шрифты. С помощью таких инструментов, как Transfonter, очень просто создать начертание шрифта.
Создание динамического контента
Итак, у вас есть рендерер и макет. Теперь вы, вероятно, захотите наполнить последнее динамическим контентом. В противном случае создание PDF в большинстве случаев не имело бы особого смысла.
Скорее всего, вы уже находитесь в среде веб-приложения, когда разрабатываете свой PDF-файл. В этом случае у вас, скорее всего, уже есть средства для создания и обслуживания динамических HTML-шаблонов.
Если вы этого не сделаете, вы можете использовать комбинацию Express и шаблонизатора, такого как Pug или Mustache.
Ваш код Node.js, вероятно, будет выглядеть примерно так:
const express = require('express') const mustacheExpress = require('mustache-express') const puppeteer = require('puppeteer') const app = express() app.engine('html', mustacheExpress()) app.set('view engine', 'html') app.get('/export/html', (req, res) => { const templateData = { … } res.render('template.html', templateData) }) app.get('/export/pdf', (req, res) => { (async () => { const browser = await puppeteer.launch() const page = await browser.newPage() await page.goto('http://localhost:3000/export/html') const buffer = await page.pdf({format: 'A4', …}) res.type('application/pdf') res.send(buffer) browser.close() })() }) app.listen(3000)
Вот и все. Все готово. Вызов localhost: 3000 / export / pdf запустит браузер Chromium без заголовка, вызовет l ocalhost: 3000 / export / html и отобразит его содержимое в PDF, а затем отправьте его обратно в браузер пользователя.
Конечно, вы можете захотеть сделать другие вещи с вашим PDF-файлом, например, сохранить его на диск или отправить по электронной почте. В этом случае вам может не понадобиться маршрут для / export / pdf, но базовая механика останется прежней.
И последнее: я счел целесообразным добавить еще одну опцию в метод goto Puppeteer:
page.goto('…', {waitUntil: 'networkidle0'})
Параметр waitUntil networkidle0 указывает Puppeteer считать, что страница полностью загружена, только если не было открытого сетевого подключения в течение как минимум 500 мс. Доступны и другие опции, которые могут быть более полезными в разных случаях.
Заключение
Используя, вероятно, наиболее распространенный механизм рендеринга для всех видов макетов, вы можете избавить себя от многих проблем при создании PDF-файлов. Дело не в том, что макеты на основе CSS всегда безболезненны и понятны, но у вас, вероятно, уже есть некоторый опыт работы с ними. Кроме того, вам не нужно беспокоиться о кросс-браузерной совместимости и тому подобном при создании макетов для одной единственной версии браузера - ваш макет должен работать только в одном конкретном месте. Это дает вам свободу легко создавать сложные и профессиональные макеты PDF для ваших приложений.
Спасибо за прочтение, надеюсь, вам понравилось. Если у вас есть какие-либо вопросы или вы хотите узнать больше о любой из обсуждаемых технологий или методов, не стесняйтесь оставлять комментарии!
Вы также можете связаться со мной на веб-сайте моей компании или в Твиттере.