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

в 20:26, , рубрики: itext, java, PDF, Servlet, Веб-разработка, метки: , ,

Как 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(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 документ:

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

Автор: ruslanys

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js