Для меня было очень неожиданно то, что в хабе по Java практически нет информации по работе с PDF документами, поэтому я, из личного опыта, хочу на примере сервлета показать как легко можно любую web-страницу превратить в PDF документ.
Преамбула:
Напишем простой сервлет, который будет брать указанную нами web-страницу по HTTP протоколу и генерировать на её основе полноценный PDF документ.
Используемые библиотеки:
- Flying Saucer PDF — основная библиотека, которая поможет создать нам PDF документ из HTML/CSS
- iText — библиотека, которая включена в состав той, что описана выше, но я не мог не включить ее в список библиотек, т.к. именно на основе неё будет генерироваться PDF документ
- HTML Cleaner — библиотека, которая будет приводить наш HTML код в порядок
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.0.4</version>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlcleaner</groupId>
<artifactId>htmlcleaner</artifactId>
<version>2.6.1</version>
</dependency>
Формирование страницы:
Одним из самый важных моментов является формирование страницы. Дело в том, что именно из самой страницы, посредством CSS, задаются параметры будущего PDF документа.
Рассмотрим макет:
<%@ page import="java.util.Date" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
%>
<html>
<head>
<title>Пример</title>
<style>
@font-face {
font-family: "HabraFont";
src: url(https://localhost:8080/resources/fonts/tahoma.ttf);
-fs-pdf-font-embed: embed;
-fs-pdf-font-encoding: Identity-H;
}
@page {
margin: 0px;
padding: 0px;
size: A4 portrait;
}
@media print {
.new_page {
page-break-after: always;
}
}
body {
background-image: url(https://localhost:8080/resources/images/background.png);
}
body *{
padding: 0;
margin: 0;
}
* {
font-family: HabraFont;
}
#block {
width: 90%;
margin: auto;
background-color: white;
border: dashed #dbdbdb 1px;
}
#logo {
margin-top: 5px;
width: 100%;
text-align: center;
border-bottom: dashed #dbdbdb 1px;
}
#content {
padding-left: 10px;
}
</style>
</head>
<body>
<div id="block">
<div id="logo"><img src="http://localhost:8080/resources/images/habra-logo.png"></div>
<div id="content">
Привет! Текущее время: <%=sdf.format(new Date())%>
<div class="new_page"> </div>
Новая страница!
</div>
</div>
</body>
</html>
Здесь хочу остановиться на нескольких моментах. Для начала самое важное: все пути должны быть абсолютными! Картинки, стили, адреса шрифтов и др., на всё должны быть прописаны абсолютные пути. А теперь пройдемся по CSS правилам (то, что начинается с символа @).
@ font-face — это правило, которое скажет нашему PDF генератору какой нужно взять шрифт, и откуда. Проблема в том, что библиотека, которая будет генерировать PDF документ не содержит шрифтов, включающих в себя кириллицу. Именно поэтому таким образом придется определять ВСЕ шрифты, которые используются в Вашей странице, пусть это будут даже стандартные шрифты: Arial, Verdana, Tahoma, и пр., в противном случае Вы рискуете не увидеть кириллицу в Вашем документе.
Обратите внимание на такие свойства как "-fs-pdf-font-embed: embed;" и "-fs-pdf-font-encoding: Identity-H;", эти свойства необходимы, их просто не забывайте добавлять.
@ page — это правило, которое задает отступы для PDF документа, ну и его размер. Здесь хотелось бы отметить, что если Вы укажите размер страницы A3 (а как показывает практика, это часто необходимо, т.к. страница не помещается в документ по ширине), то это не значит, что пользователю необходимо будет распечатывать документ (при желании) в формате A3, скорее просто весь контент будет пропорционально уменьшен/увеличен до желаемого (чаще A4). Т.е. относитесь к значению свойства size скептически, но знайте, что оно может сыграть для Вас ключевую роль.
@ media — правило, позволяющее создавать CSS классы для определенного типа устройств, в нашем случае это «print». Внутри этого правила мы создали класс, после которого наш генератор PDF документа создаст новую страницу.
Сервлет:
Теперь напишем сервлет, который будет возвращать нам сгенерированный PDF документ:
package ru.habrahabr.web_to_pdf.servlets;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.PrettyXmlSerializer;
import org.htmlcleaner.TagNode;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
/**
* Date: 31.03.2014
* Time: 9:33
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
public class PdfServlet extends HttpServlet {
private static final String PAGE_TO_PARSE = "http://localhost:8080/page.jsp";
private static final String CHARSET = "UTF-8";
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
resp.setContentType("application/pdf");
byte[] pdfDoc = performPdfDocument(PAGE_TO_PARSE);
resp.setContentLength(pdfDoc.length);
resp.getOutputStream().write(pdfDoc);
} catch (Exception ex) {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.write("<strong>Something wrong</strong><br /><br />");
ex.printStackTrace(out);
ex.printStackTrace();
}
}
/**
* Метод, подготавливащий PDF документ.
* @param path путь до страницы
* @return PDF документ
* @throws Exception
*/
private byte[] performPdfDocument(String path) throws Exception {
// Получаем HTML код страницы
String html = getHtml(path);
// Буффер, в котором будет лежать отформатированный HTML код
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Форматирование HTML кода
/* эта процедура не обязательна, но я настоятельно рекомендую использовать этот блок */
HtmlCleaner cleaner = new HtmlCleaner();
CleanerProperties props = cleaner.getProperties();
props.setCharset(CHARSET);
TagNode node = cleaner.clean(html);
new PrettyXmlSerializer(props).writeToStream(node, out);
// Создаем PDF из подготовленного HTML кода
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(new String(out.toByteArray(), CHARSET));
renderer.layout();
/* заметьте, на этом этапе Вы можете записать PDF документ, скажем, в файл
* но раз мы пишем сервлет, который будет возвращать PDF документ,
* нам нужен массив байт, который мы отдадим пользователю */
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
renderer.createPDF(outputStream);
// Завершаем работу
renderer.finishPDF();
out.flush();
out.close();
byte[] result = outputStream.toByteArray();
outputStream.close();
return result;
}
private String getHtml(String path) throws IOException {
URLConnection urlConnection = new URL(path).openConnection();
((HttpURLConnection) urlConnection).setInstanceFollowRedirects(true);
HttpURLConnection.setFollowRedirects(true);
boolean redirect = false;
// normally, 3xx is redirect
int status = ((HttpURLConnection) urlConnection).getResponseCode();
if (HttpURLConnection.HTTP_OK != status &&
(HttpURLConnection.HTTP_MOVED_TEMP == status ||
HttpURLConnection.HTTP_MOVED_PERM == status ||
HttpURLConnection.HTTP_SEE_OTHER == status)) {
redirect = true;
}
if (redirect) {
// get redirect url from "location" header field
String newUrl = urlConnection.getHeaderField("Location");
// open the new connnection again
urlConnection = new URL(newUrl).openConnection();
}
urlConnection.setConnectTimeout(30000);
urlConnection.setReadTimeout(30000);
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), CHARSET));
StringBuilder sb = new StringBuilder();
String line;
while (null != (line = in.readLine())) {
sb.append(line).append("n");
}
return sb.toString().trim();
}
@Override
public String getServletInfo() {
return "The servlet that generate and returns pdf file";
}
}
Кстати, совсем не обязательно писать для этих целей сервлет, Вы можете перенести логику этого сервлета хоть в консольное приложение, которое будет сохранять PDF документы в файлы. Как Вы могли заметить, в сервлете не нужно ничего настраивать, менять, дополнять, и т.д. (ну за исключением пути до страницы), соответственно вся работа по подготовке PDF документа очень проста и происходит исключительно во вьюшке.
В конечном итоге у Вас должен получиться примерно такой PDF документ: db.tt/HGMKTqYV
Я немного дополнил свой документ информацией (распарсил главную страницу Хабра) и у меня получился такой вот документ: db.tt/WKIOUg96
Ссылка на исходники: db.tt/FbP6vwQ6
P.S. В принципе, на основе этого примера можно написать целый сервис, который будет по любому адресу страницы создавать PDF документ. Единственное, что будет необходимо сделать — это привести HTML код страницы в соответствие с нашими правилами, т.е. в первую очередь нужно будет переписать все относительные пути на абсолютные (благо это делается не сложно), и в соответствии с какой-то логикой задать размеры документа.
Автор: ruslanys