Для меня было очень неожиданно то, что в хабе по 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(http://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(http://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 код страницы в соответствие с нашими правилами, т.е. в первую очередь нужно будет переписать все относительные пути на абсолютные (благо это делается не сложно), и в соответствии с какой-то логикой задать размеры документа.
ссылка на оригинал статьи http://habrahabr.ru/post/217561/
Добавить комментарий