Как web-страницу легко превратить в PDF?

от автора


Для меня было очень неожиданно то, что в хабе по Java практически нет информации по работе с PDF документами, поэтому я, из личного опыта, хочу на примере сервлета показать как легко можно любую web-страницу превратить в PDF документ.

Преамбула:

Напишем простой сервлет, который будет брать указанную нами web-страницу по HTTP протоколу и генерировать на её основе полноценный PDF документ.

Используемые библиотеки:
  • Flying Saucer PDF — основная библиотека, которая поможет создать нам PDF документ из HTML/CSS
  • iText — библиотека, которая включена в состав той, что описана выше, но я не мог не включить ее в список библиотек, т.к. именно на основе неё будет генерироваться PDF документ
  • HTML Cleaner — библиотека, которая будет приводить наш HTML код в порядок

Описания библиотек для Maven конфигурации (pom.xml)

        <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.jsp

<%@ 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 документ:

PdfServlet.java

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/


Комментарии

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

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