AI Как приручить iText8: превращаем HTML в PDF без седых волос

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 641
Лучшие ответы
0
Реакции
0
Баллы
243
Offline
#1

Введение — Зачем нам вообще PDF?


Представьте ситуацию: пятница, вечер, до релиза осталось два дня. Заказчик внезапно вспоминает, что «было бы неплохо генерировать договоры в PDF». Знакомо?

Я оказался в похожей ситуации год назад. Задача казалась тривиальной: взять HTML-шаблон счёта, подставить данные и получить красивый PDF. «Часа на два работы», — подумал я. Как ошибался...

Скрытый текст

Почему вообще нужна генерация PDF? Типичные сценарии:


  • Счета, квитанции, договоры — всё, что нужно распечатать или отправить клиенту


  • Отчёты с графиками и таблицами


  • «Скриншоты» страниц для архивирования


  • Сертификаты, дипломы, бейджи

Почему HTML — отличный промежуточный формат? Мы уже знаем HTML и CSS. Есть мощные шаблонизаторы вроде Thymeleaf. Дизайнеры могут править шаблоны без Java-разработчика. И самое главное — можно увидеть результат в браузере до конвертации.

Какие альтернативы существуют?


  • Flying Saucer — старожил рынка, LGPL-лицензия (можно в коммерческих проектах), но ограниченная поддержка CSS


  • OpenPDF — форк старого iText 4.x, тоже LGPL, но не умеет конвертировать HTML из коробки


  • wkhtmltopdf — идеальный рендеринг CSS3 и JavaScript, но требует установки бинарника


  • Apache PDFBox — низкоуровневая библиотека для работы с PDF, без поддержки HTML


  • iText8 + html2pdf — мощный комбайн с хорошей CSS-поддержкой и активной разработкой

Я выбрал iText8, потому что он активно развивается, имеет отличную документацию и поддержку CSS близкую к браузерной. Но, как выяснилось, у этого выбора есть свои подводные камни — о них и поговорим.

Подключаем iText8 — первые грабли в pom.xml


Теория — это прекрасно, но давайте сразу к делу. Прежде чем писать код, разберёмся с зависимостями. Здесь нас ждёт первый сюрприз.

iText8 vs iText7 vs iText5 — почему версии это важно


В мире iText существует несколько поколений библиотек:


  • iText 5.x — старая версия, больше не поддерживается


  • iText 7.x — полностью переписанная версия с новым API


  • iText 8.x — актуальная версия с улучшениями производительности

Не смешивайте их! Если в проекте есть зависимости от iText5, а вы добавляете iText8 — готовьтесь к ClassNotFoundException и часам дебага.

html2pdf — отдельная библиотека


Сам iText8 не умеет конвертировать HTML в PDF. Для этого существует отдельный модуль html2pdf, который тянет за собой itext-core.

Пример минимального pom.xml:

<dependencies>
<!-- html2pdf тянет itext-core транзитивно -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>5.0.5</version>
</dependency>

<!-- Опционально: если нужна поддержка SVG -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>svg</artifactId>
<version>8.0.5</version>
</dependency>
</dependencies>

Важно: версии html2pdf и модулей itext-core должны быть совместимы.

⚠️ Лицензирование — читать обязательно!


Вот где большинство разработчиков получает неприятный сюрприз. iText8 распространяется под лицензией AGPL v3. Что это значит на практике?

AGPL требует:


  • Если вы используете iText8 в своём приложении и предоставляете его пользователям (включая SaaS!) — весь ваш код должен быть открыт под AGPL


  • Это касается даже сетевого взаимодействия — не только распространения бинарников

Когда нужна коммерческая лицензия:


  • Закрытые проекты (вы не хотите открывать исходный код)


  • SaaS-приложения


  • Продажа ПО с iText8 внутри

Альтернативы для коммерческих проектов:

Библиотека

Лицензия

Комментарий

iText8 Commercial​

Коммерческая​

От ~$2000/год​

OpenPDF​

LGPL/MPL​

Можно использовать в закрытых проектах​

Flying Saucer​

LGPL​

Подходит для коммерческих проектов​

Apache PDFBox​

Apache 2.0​

Полностью свободная, но без HTML в PDF​


Первая конвертация — «Hello, PDF!»


С зависимостями разобрались. Теперь давайте уже напишем код!

HtmlConverter — точка входа


Главный класс для конвертации — HtmlConverter. У него есть несколько перегрузок convertToPdf():

// Из строки в файл
HtmlConverter.convertToPdf(htmlString, outputStream);

// Из InputStream в OutputStream
HtmlConverter.convertToPdf(inputStream, outputStream);

// С настройками
HtmlConverter.convertToPdf(htmlString, outputStream, converterProperties);
Минимальный рабочий пример


package com.example.itext;

import com.itextpdf.html2pdf.HtmlConverter;

import java.io.FileOutputStream;
import java.io.IOException;

public class BasicHtmlToPdf {

public static void main(String[] args) {
String html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Первый PDF</title>
<style>
body { font-family: sans-serif; padding: 20px; }
h1 { color: #2c3e50; }
</style>
</head>
<body>
<h1>Привет, PDF!</h1>
<p>Это мой первый PDF-документ, созданный из HTML.</p>
</body>
</html>
""";

try (FileOutputStream outputStream = new FileOutputStream("hello.pdf")) {
HtmlConverter.convertToPdf(html, outputStream);
System.out.println("PDF успешно создан!");
} catch (IOException e) {
e.printStackTrace();
}
}
}

Запустите — и вуаля! В текущей директории появится hello.pdf. Всего 15 строк кода, и у нас есть рабочая конвертация.

Сервис для Spring Boot


В реальном приложении нам нужен сервис, который можно инжектить. Вот production-ready версия:

@Service
public class HtmlToPdfService {

public ByteArrayOutputStream convertHtmlToPdf(String html, String baseUri)
throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

ConverterProperties properties = new ConverterProperties();
if (baseUri != null && !baseUri.isEmpty()) {
properties.setBaseUri(baseUri);
}

try (var inputStream = new ByteArrayInputStream(
html.getBytes(StandardCharsets.UTF_8))) {
HtmlConverter.convertToPdf(inputStream, outputStream, properties);
}

return outputStream;
}
}
Скрытый текст

ByteArrayOutputStream удобен тем, что его можно:


  • Вернуть через REST-контроллер как byte[]


  • Сохранить в файл


  • Отправить в S3 или MinIO


  • Передать дальше по цепочке обработки
Обработка исключений — чтобы не было мучительно больно


Знаете, что хуже нерабочего PDF? Непонятная ошибка NullPointerException без контекста в логах в три часа ночи. Давайте сделаем обработку ошибок человеческой:

@Service
public class SafeHtmlToPdfService {

private static final Logger log = LoggerFactory.getLogger(SafeHtmlToPdfService.class);

public ByteArrayOutputStream convertHtmlToPdfSafe(String html, String baseUri) {
log.debug("Начинаем конвертацию. Размер HTML: {} символов", html.length());

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ConverterProperties properties = new ConverterProperties();

if (baseUri != null) {
properties.setBaseUri(baseUri);
}

try (var inputStream = new ByteArrayInputStream(
html.getBytes(StandardCharsets.UTF_8))) {
HtmlConverter.convertToPdf(inputStream, outputStream, properties);
log.info("PDF создан. Размер: {} байт", outputStream.size());
return outputStream;

} catch (Html2PdfException e) {
log.error("Ошибка парсинга HTML/CSS: {}", e.getMessage());
throw new PdfGenerationException("Невалидный HTML: " + e.getMessage(), e);

} catch (IOException e) {
log.error("Ошибка I/O при конвертации: {}", e.getMessage());
throw new PdfGenerationException("Ошибка генерации PDF", e);
}
}
}

Обратите внимание: мы логируем размер HTML на входе и размер PDF на выходе. Это поможет при отладке проблем с производительностью.

Thymeleaf + iText8 — динамические шаблоны


Итак, у нас есть базовая конвертация. Но статичный HTML — это мило, а в реальной жизни нам нужны динамические данные. Счёт должен содержать имя клиента, список товаров, итоговую сумму. Здесь на сцену выходит Thymeleaf.

Почему Thymeleaf?


  • Знакомый синтаксис (если вы работали со Spring MVC)


  • Шаблоны можно открыть в браузере без сервера


  • Мощная логика: циклы, условия, форматирование


  • Хорошая интеграция с Spring
Настройка TemplateEngine


Для генерации PDF нам не нужен Spring MVC. Настроим TemplateEngine программно:

public class TemplateEngineConfig {

public static TemplateEngine createTemplateEngine(String templatesPrefix) {
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();

// Путь к шаблонам в resources
resolver.setPrefix(templatesPrefix);
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(true); // В проде — true!

TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);

return engine;
}
}

Шаблоны должны лежать в src/main/resources/templates/ (или в указанном prefix).

Пример: генерация счёта


Допустим, у нас есть шаблон invoice.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
<head>
<meta charset="UTF-8"/>
<title th:text="'Счёт ' + ${invoice.invoiceNumber}">Счёт</title>
<style>
body { font-family: sans-serif; }
.header { border-bottom: 2px solid #2c3e50; }
.items-table { width: 100%; border-collapse: collapse; }
.items-table th, .items-table td {
border: 1px solid #ddd;
padding: 10px;
}
</style>
</head>
<body>
<div class="header">
<h1>Счёт <span th:text="${invoice.invoiceNumber}">INV-001</span></h1>
<p th:text="${#temporals.format(invoice.invoiceDate, 'dd.MM.yyyy')}">01.01.2024</p>
</div>

<h2>Заказчик:</h2>
<p th:text="${invoice.customer.name}">Имя клиента</p>

<table class="items-table">
<tr th:each="item : ${invoice.items}">
<td th:text="${item.description}">Товар</td>
<td th:text="${item.quantity}">1</td>
<td th:text="${#numbers.formatDecimal(item.price, 1, 2)} + ' ₽'">0.00 ₽</td>
</tr>
</table>

<p><strong>Итого: </strong>
<span th:text="${#numbers.formatDecimal(invoice.totalAmount, 1, 2)} + ' ₽'">0.00 ₽</span>
</p>
</body>
</html>

И код генерации:

public class InvoiceGenerator {

private final TemplateEngine templateEngine;

public InvoiceGenerator() {
this.templateEngine = TemplateEngineConfig.createTemplateEngine("templates/");
}

public byte[] generateInvoicePdf(InvoiceData invoice) throws IOException {
// Шаг 1: Создаём контекст с данными
Context context = new Context(new Locale("ru"));
context.setVariable("invoice", invoice);

// Шаг 2: Рендерим шаблон в HTML
String htmlContent = templateEngine.process("invoice", context);

// Шаг 3: Конвертируем в PDF
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream();
ConverterProperties properties = new ConverterProperties();

String baseUri = getClass().getClassLoader()
.getResource("templates/").toString();
properties.setBaseUri(baseUri);

HtmlConverter.convertToPdf(htmlContent, pdfOutput, properties);

return pdfOutput.toByteArray();
}
}

Процесс простой: DTO в Thymeleaf в HTML-строка в iText8 в PDF. Каждый шаг можно отладить отдельно.

Когда Thymeleaf капризничает


Thymeleaf может упасть по разным причинам: шаблон не найден, переменная отсутствует, синтаксическая ошибка. Важно обрабатывать эти ошибки:

public byte[] generatePdfSafe(String templateName, Map<String, Object> variables) {
try {
Context context = new Context(new Locale("ru"));
variables.forEach(context::setVariable);

String html = templateEngine.process(templateName, context);
return convertHtmlToPdf(html);

} catch (TemplateInputException e) {
log.error("Шаблон '{}' не найден", templateName);
return generateErrorPdf("Шаблон не найден", templateName);

} catch (TemplateEngineException e) {
log.error("Ошибка в шаблоне '{}': {}", templateName, e.getMessage());
return generateErrorPdf("Ошибка шаблона", e.getMessage());
}
}

Метод generateErrorPdf() создаёт PDF с сообщением об ошибке — пользователь получит хоть что-то, а не 500-ю ошибку.

Стили и CSS — где iText8 говорит «нет»


Шаблоны настроены, данные подставляются. А теперь — самое интересное. Вы сверстали красивый шаблон с flexbox и grid, открыли в браузере — красота! Конвертировали в PDF — и получили кашу. Знакомьтесь: ограничения CSS в iText8.

Что работает


Хорошие новости — базовый CSS работает отлично:

/* ✅ Работает */
body {
font-family: sans-serif;
font-size: 12pt;
color: #333;
}

table {
width: 100%;
border-collapse: collapse;
}

.box {
border: 1px solid #ddd;
border-radius: 8px; /* Да, border-radius работает! */
padding: 20px;
margin: 10px;
}

position: absolute; /* Работает */
position: fixed; /* Работает — фиксация относительно страницы PDF */
Что НЕ работает


А вот плохие новости:

/* ❌ НЕ работает */
display: flex;
display: grid;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
text-shadow: 1px 1px 2px #000;

/* ❌ @media queries бессмысленны */
@media print { ... } /* PDF — не экран, игнорируется */

Flexbox и Grid — это боль. Придётся использовать таблицы для раскладки, как в 2005-м. Да, я тоже вздохнул.

@page — ваш друг для PDF


Есть специальное CSS-правило, которое работает только в PDF:

@page {
size: A4; /* Размер страницы */
margin: 20mm 15mm; /* Поля */
}

@page :first {
margin-top: 30mm; /* Больше отступ на первой странице */
}
baseUri — решаем проблему путей


Если в CSS есть background-image: url('images/bg.png'), iText8 должен знать, где искать этот файл. Для этого используется baseUri:

ConverterProperties properties = new ConverterProperties();

// Для ресурсов из classpath
String baseUri = getClass().getClassLoader()
.getResource("templates/").toString();
properties.setBaseUri(baseUri);

// Или для файловой системы
properties.setBaseUri("file:///var/www/templates/");

Без правильного baseUri изображения и внешние CSS просто не загрузятся.

Работа с изображениями — когда картинка не хочет появляться


CSS разобрали, теперь изображения. Они в PDF — отдельная история. Могут не загрузиться, быть слишком большими или просто сломать рендеринг.

Типичные проблемы


  1. Относительные пути — работают только с правильным baseUri


  2. 404 ошибки — если изображение недоступно, PDF может сломаться


  3. Большие файлы — 10MB картинка = 10MB PDF = медленная генерация


  4. Внешние URL — сетевые таймауты при генерации
Валидация изображений перед конвертацией


Разумная практика — проверить доступность всех изображений до конвертации:

public class ImageValidator {

public boolean isImageAccessible(String imageUrl, int timeoutMs) {
if (imageUrl.startsWith("data:")) {
return true; // Base64 всегда "доступен"
}

try {
URL url = URI.create(imageUrl).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);

int responseCode = connection.getResponseCode();
return responseCode >= 200 && responseCode < 400;

} catch (IOException e) {
return false;
}
}
}
Base64-кодирование — когда это оправдано


Иногда проще встроить изображение прямо в HTML:

public String encodeToDataUri(byte[] imageBytes, String mimeType) {
String base64 = Base64.getEncoder().encodeToString(imageBytes);
return "data:" + mimeType + ";base64," + base64;
}
Скрытый текст

Используйте Base64 для:


  • Маленьких изображений (< 100KB)


  • Email-вложений (один файл = один PDF)


  • Ситуаций, когда внешние ресурсы недоступны

Не используйте для:


  • Больших изображений (раздует PDF)


  • Повторяющихся изображений (лучше кэшировать)
Предобработка HTML с Jsoup


Перед конвертацией полезно «почистить» HTML и заменить недоступные изображения на placeholder:

public String replaceInvalidImages(String html, String placeholder) {
Document doc = Jsoup.parse(html);
Elements images = doc.select("img[src]");

for (Element img : images) {
String src = img.attr("src");

if (!src.startsWith("data:") && !imageValidator.isImageAccessible(src)) {
log.warn("Изображение недоступно: {}", src);
img.attr("src", placeholder);
img.attr("data-original-src", src); // Сохраняем для дебага
}
}

return doc.html();
}
Размер страницы — не только A4


Изображения приручили, теперь разберёмся с размерами. По умолчанию iText8 создаёт PDF с размером A4. Но что если нужен Letter, A5, или вообще кастомный размер для «скриншота»?

PdfDocument и PageSize


Для кастомных размеров нам понадобится работать с PdfDocument напрямую:

public ByteArrayOutputStream convertWithCustomSize(
String html,
float widthPoints,
float heightPoints) throws IOException {

ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream();

// Создаём PdfWriter и PdfDocument
PdfWriter writer = new PdfWriter(pdfOutput);
PdfDocument pdfDoc = new PdfDocument(writer);

// Устанавливаем размер страницы
PageSize customSize = new PageSize(widthPoints, heightPoints);
pdfDoc.setDefaultPageSize(customSize);

// Конвертируем
ConverterProperties properties = new ConverterProperties();
try (var inputStream = new ByteArrayInputStream(
html.getBytes(StandardCharsets.UTF_8))) {
HtmlConverter.convertToPdf(inputStream, pdfDoc, properties);
}

return pdfOutput;
}
Скрытый текст
Единицы измерения


В iText размеры указываются в points (пунктах):


  • 1 дюйм = 72 points


  • 1 мм ≈ 2.83 points

Стандартные размеры:


  • A4: 595 × 842 points (210 × 297 мм)


  • Letter: 612 × 792 points (8.5 × 11 дюймов)


  • A5: 420 × 595 points (148 × 210 мм)
Кейс: генерация «скриншотов»


Для превью документов часто нужен фиксированный размер, например 760 × 370 points:

public ByteArrayOutputStream convertToScreenshot(String html) throws IOException {
return convertWithCustomSize(html, 760, 370);
}

Такие «скриншоты» отлично подходят для встраивания в другие документы или для отображения превью в интерфейсе.

Шрифты — кириллица и другие приключения


Размеры настроили, а теперь — моя любимая история. Знаете, что я увидел, когда впервые попробовал вывести русский текст? Квадратики. Много квадратиков. «□□□□□ □□□!» вместо «Привет, мир!»

Проблема в том, что стандартные PDF-шрифты (Helvetica, Times, Courier) не содержат кириллицы.

FontProvider — спасение


FontProvider управляет шрифтами в iText8. DefaultFontProvider включает базовые шрифты и (опционально) шрифты из поставки iText:

public ConverterProperties createConverterPropertiesWithFonts() {
FontProvider fontProvider = new DefaultFontProvider(
true, // Стандартные PDF-шрифты
true, // Шрифты iText (включая кириллицу!)
false // Системные шрифты (медленно)
);

ConverterProperties properties = new ConverterProperties();
properties.setFontProvider(fontProvider);

return properties;
}

Второй параметр (true) — ключевой. Он включает шрифты, поставляемые с iText, среди которых есть поддерживающие кириллицу.

Добавление кастомных шрифтов


Если нужен конкретный шрифт (корпоративный, например):

FontProvider fontProvider = new DefaultFontProvider(true, true, false);

// Добавляем шрифт из resources
String fontPath = getClass().getClassLoader()
.getResource("fonts/CustomFont.ttf").toString();
fontProvider.addFont(fontPath);

// Или целую директорию
fontProvider.addDirectory("/path/to/fonts/");
Когда шрифт не хочет загружаться


Шрифт может не загрузиться по разным причинам: файл не найден, неправильный формат, нет прав. Важно это обрабатывать:

public FontProvider createFontProviderWithFallback(String primaryFontPath) {
FontProvider fontProvider = new DefaultFontProvider(true, true, false);

try {
if (Files.exists(Path.of(primaryFontPath))) {
fontProvider.addFont(primaryFontPath);
log.info("Загружен шрифт: {}", primaryFontPath);
} else {
log.warn("Шрифт не найден: {}, используем встроенные", primaryFontPath);
}
} catch (Exception e) {
log.warn("Ошибка загрузки шрифта: {}", e.getMessage());
}

return fontProvider;
}
Production практики


Мы прошли долгий путь: от базовой конвертации до шрифтов и изображений. Теперь поговорим о том, как сделать генерацию PDF надёжной и быстрой в production.

Кэширование TemplateEngine и FontProvider


Создание TemplateEngine и FontProvider — дорогие операции. Не создавайте их на каждый запрос!

@Configuration
public class PdfGenerationConfig {

@Bean
@Lazy
public TemplateEngine pdfTemplateEngine() {
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix("templates/pdf/");
resolver.setSuffix(".html");
resolver.setCacheable(true); // Кэшируем шаблоны!

TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);
return engine;
}

@Bean
@Lazy
public FontProvider pdfFontProvider() {
// Создаём один раз — переиспользуем везде
return new DefaultFontProvider(true, true, false);
}
}

@Lazy — чтобы не тратить время на старте, если PDF не нужны сразу.

Асинхронная генерация


Для высоконагруженных систем генерация PDF должна быть асинхронной:

@Service
public class AsyncPdfService {

@Async("pdfExecutor")
public CompletableFuture<byte[]> generatePdfAsync(String html) {
try {
byte[] pdf = generatePdfSync(html);
return CompletableFuture.completedFuture(pdf);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}

Важно: @Async("pdfExecutor") требует настроенного bean'а с именем pdfExecutor. Добавьте конфигурацию:

@Configuration
@EnableAsync
public class AsyncConfig {

@Bean(name = "pdfExecutor")
public Executor pdfExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 10, // core и max размер пула
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // очередь задач
new ThreadPoolExecutor.CallerRunsPolicy()
);
return executor;
}
}

Пул потоков должен быть ограничен — генерация PDF съедает память.

Retry-логика с exponential backoff


Если HTML содержит внешние ресурсы, сетевые ошибки неизбежны. Retry помогает:

public byte[] convertWithRetry(String html, int maxAttempts) {
int attempt = 0;
Exception lastException = null;

while (attempt < maxAttempts) {
attempt++;
try {
return convertPdf(html);
} catch (IOException e) {
lastException = e;
if (isRetryable(e) && attempt < maxAttempts) {
long delay = (long) (1000 * Math.pow(2, attempt - 1)); // Экспоненциальная задержка
Thread.sleep(delay);
}
}
}

throw new PdfGenerationException("Не удалось создать PDF", lastException);
}
Метрики с Micrometer


Без метрик вы не узнаете о проблемах, пока пользователи не начнут жаловаться:

@Service
public class MeteredPdfService {

private final Timer pdfGenerationTimer;
private final Counter pdfSuccessCounter;
private final Counter pdfFailureCounter;

public MeteredPdfService(MeterRegistry registry) {
this.pdfGenerationTimer = Timer.builder("pdf.generation.time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// ... остальные метрики
}

public byte[] generatePdf(String html) {
return pdfGenerationTimer.record(() -> {
try {
byte[] pdf = doGeneratePdf(html);
pdfSuccessCounter.increment();
return pdf;
} catch (Exception e) {
pdfFailureCounter.increment();
throw e;
}
});
}
}

Рекомендую следить за:


  • Время генерации (среднее, p95, p99)


  • Размер файлов


  • Частота ошибок


  • Количество активных генераций
Когда iText8 — не лучший выбор


iText8 — мощный инструмент, но не универсальный. Вот когда стоит посмотреть на альтернативы:

Сценарий

Лучший выбор

Почему

Сложные CSS3 (flexbox, grid)​

wkhtmltopdf, Puppeteer​

Полный браузерный рендеринг​

Закрытый коммерческий проект​

Flying Saucer, OpenPDF​

LGPL лицензия​

Минимальные требования к CSS​

Flying Saucer​

Проще, легче​

PDF с JavaScript​

Headless Chrome​

JS исполняется​

Максимальный контроль над PDF​

iText8​

Программный API​


Бонус — PDF в изображение


Мы почти закончили. Но напоследок — приятный бонус. Иногда нужно не просто PDF, а изображение: превью для галереи, миниатюра для email, «скриншот» для отчёта. Apache PDFBox поможет.

PDFBox: рендеринг страниц


public List<BufferedImage> renderPdfToImages(byte[] pdfBytes, float dpi)
throws IOException {
List<BufferedImage> images = new ArrayList<>();

try (PDDocument document = Loader.loadPDF(pdfBytes)) {
PDFRenderer renderer = new PDFRenderer(document);

for (int page = 0; page < document.getNumberOfPages(); page++) {
BufferedImage image = renderer.renderImageWithDPI(page, dpi, ImageType.RGB);
images.add(image);
}
}

return images;
}
Скрытый текст

DPI влияет на качество и размер:


  • 72 DPI — для превью, быстро, маленький размер


  • 150 DPI — хороший баланс


  • 300 DPI — для печати, большие файлы
Полный pipeline: HTML в PDF в Image


public byte[] createPreviewImage(String html) throws IOException {
// HTML в PDF
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream();
HtmlConverter.convertToPdf(html, pdfOutput);

// PDF в Image
BufferedImage image = renderFirstPage(pdfOutput.toByteArray(), 150f);

// Image в PNG bytes
ByteArrayOutputStream imageOutput = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", imageOutput);

return imageOutput.toByteArray();
}
Сохранение в разных форматах


// PNG — без потерь, поддержка прозрачности
ImageIO.write(image, "PNG", outputStream);

// JPEG — меньше размер, без прозрачности
ImageIO.write(ensureRgb(image), "JPEG", outputStream);

Для JPEG нужно конвертировать изображение в RGB, иначе будут артефакты.

Заключение и чеклист


Мы прошли путь от «Hello, PDF» до production-ready сервиса с метриками и retry-логикой. Давайте подведём итоги.

Когда использовать iText8


Используйте, если:


  • Нужна хорошая поддержка CSS (но не flexbox/grid)


  • Важна производительность


  • Есть бюджет на коммерческую лицензию (или проект open source)


  • Нужен полный программный контроль над PDF

Не используйте, если:


  • Закрытый проект без бюджета на лицензию


  • Критичен идеальный CSS3-рендеринг


  • Нужен JavaScript в шаблонах
Чеклист для нового проекта


Перед началом:


  • Проверить лицензию — AGPL подходит?


  • Определить целевые форматы страниц


  • Понять, какие CSS-фичи нужны

При настройке:


  • Правильные версии зависимостей в pom.xml


  • FontProvider с поддержкой кириллицы


  • Singleton для TemplateEngine и FontProvider


  • Корректный baseUri для ресурсов

В production:


  • Обработка всех типов исключений


  • Логирование с контекстом


  • Метрики генерации


  • Таймауты для внешних ресурсов


  • Retry-логика при сетевых ошибках


  • Ограничение пула потоков
Типичные грабли

Проблема

Решение

Квадратики вместо кириллицы​

DefaultFontProvider(true, true, false)​

Изображения не загружаются​

Проверить baseUri​

Flexbox не работает​

Использовать таблицы​

PDF пустой или сломанный​

Валидировать HTML через Jsoup​

Медленная генерация​

Кэшировать TemplateEngine, FontProvider​

OutOfMemoryError​

Ограничить размер HTML и изображений​



Надеюсь, эта статья сэкономит вам пару бессонных ночей. Если у вас есть свои кейсы или вопросы по iText8 — делитесь в комментариях! Особенно интересно узнать, с какими подводными камнями столкнулись вы. А может, вы нашли элегантное решение для flexbox-раскладки в PDF? Расскажите!
 
Автор темы Похожие темы Форум Ответов Дата
AI Overview AI 0

Похожие темы

Яндекс.Метрика Рейтинг@Mail.ru
Сверху Снизу