wkhtmltopdf+itext实现html生成pdf文件的打印下载(适用于linux及windows)

目中遇到个根据html转Java的功能,在java中我们itext可以快速的实现pdf打印下载的功能,在itext中我们一般有以下三中方式实现

  • 配置pdf模板,通过Adobe Acrobat 来设置域最后通过代码将数据填充进去
  • 通过FreeMarker或thymeleaf配置html模板填充数据
  • Jsoup+XMLWorkerHelper

对于上述的三种方式,我简述下我的体验:第一种方式对于入门简单,如果我们需求中的pdf文件是表格或者报表的样式还是很好实现的,但如果遇到要求和html样式一致的话就基本歇菜了。第二张方式比较理想,在项目基本完工的情况下再去改成模板不太现实,而且我只是个做后端的还没那么大的能耐,不选。第三种方式我也尝试了下,对于一些简单网页比如说博客,我们通过Jsoup可以获取到文章的内容和html样式,网上的demo也是通过博客来举例的,效果不错,但我看了下博客内容的基本样式就是一些基本的div、p、li这些标签,但在我实际的项目中样式比较复杂,生成的pdf无法打开。而且上面的三种方式都有一个致命的缺点:那就是需要去针对模板。现实的项目中我们可能多个地方需要实现pdf打印的功能,样式模板的配置将占用很大的开发工作量。第三种方式虽说不需要模板,但通过网页的标签去获取数据也会变得多样化,完全不能实现方法的复用。当然,itext也不是一无是处,在这个项目里面我们还是需要用到itext去生成水印、加密、不可编辑等一系列细致的活。

这里我想要的理想的pdf打印效果是:最少的改动,实现pdf的打印的效果与我请求的url的html样式一致。那就要使用到我们今天的主角wkhtmltopdf,完美的解决了我的难题,windows下解压即用,linux下需要安装三个依赖和一个宋体文字,下面让我们一起coding吧~

详细步骤:

  • windows下wkhtmltopdf的下载安装测试
  • linux下的wkhtmltopdf的下载安装测试
  • 配置pdf生成的路径解读
  • 自动调用java中Runtime.getRuntime()运行wkhtmltopdff插件的具体实现

windows下安装测试

通过https://wkhtmltopdf.org/downloads.html下载并解压缩wkhtmltox文件,通过cmd 命令找到 bin下面的wkhtmltopdf.exe 文件,执行wkhtmltopdf.exe +http url + pdf路径及文件名

http://image.tianluzhi.com:81/windows.mp4windows演示地址

Linux下安装测试

wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.4/wkhtmltox-0.12.4.tar.bz2用wget下载wkhtmltopdf安装,这里我们采用的是0.12.4的版本,没有和上面windows的0.12.5保持一致原因是没有0.12.5的tar包,rpm的不习惯。安装完之后我们参考上面windows的方式运行,运行过程中在centos 7中会遇到缺少依赖的问题。执行yum -y install libXrender* libfontconfig* libXext*安装后重新运行测试,还会遇到中文乱码问题,这这里我们还需将windows下的simsun字体放置到/usr/share/fonts/ ,windows字体的存放位置为C:\Windows\Fonts,win10部分用户的simsun字体是没效果的,这里给大家提供一个下载http://image.tianluzhi.com:81/SIMSUN.TTC,linux下一般就这些问题。这里就不做演示了,我们开始真正的coding吧。

配置pdf生成的路径解读


# linux path
#wkhtmltopdfPath 指向wkhtmltopdfPath安装的bin目录下wkhtmltopdf
wkhtmltopdfPath=/home/wkhtmltox/bin/wkhtmltopdf
#pdf
pdfLocationPathForLinux=/home/prd-gehcpp-01/otoResources/pdf/
waterPdfLocationPathForLinux=/home/prd-gehcpp-01/otoResources/waterPdf/

# windows path
#photoLocationPath=D://otoResources//photo//
#pdf文件路径
pdfLocationPath= D://otoResources//pdf//
#添加水印后文件路径
waterPdfLocationPath= D://otoResources//waterPdf//

上面配置中我们分别配置了linux和windows两套路径环境,在我们代码中我们会自动判别当前服务器的操作系统,然后选择相关的路径。下面我们分别解读下各个配置参数

wkhtmltopdfPath :指向wkhtmltopdfPath安装的bin目录下wkhtmltopdf,这段地址其实就是为了执行wkhtmltopdf插件而添加的,这里我们用全路径,这样在linux的环境下我们就不需要去配置环境了,另外细心的朋友会发现windows的没有配置,这是因为我已经将插件放到了资源文件夹下,
                  然后通过Java程序去拼接了一个完整的路径,具体我们往下看。
pdfLocationPathForLinux/photoLocationPath : 这两个路径分别指向了linux和Windows下的pdf生成的初始文件,如果我们不需要对pdf进行二次操作的话,在这里我们就可以把结果返回给用户。
waterPdfLocationPathForLinux/waterPdfLocationPath :这个路径是指经过加工后的路径,比如水印、加密、不可编辑等,上面的路径相当于是模子,这个才是最终返回客户的结果。

代码实现

部分代码参考 https://www.cnblogs.com/xionggeclub/p/6144241.html
controller

/**<br> * <br> * @param response<br> * @param httpUrl 需要生成pdf的请求地址<br> * @param downName 生成pdf后的文件名<br> * @return<br> */<br>@RequestMapping("pdfConvert")<br>@ResponseBody<br>public Response htmlToPdf(HttpServletResponse response ,@RequestParam String httpUrl,String downName) {<br>    return  geFileService.htmlToPdf(response,httpUrl,downName);<br>}

service

Response htmlToPdf(HttpServletResponse response, String httpUrl,String downName);

impl


package com.ge.service.impl;<br><br>import com.common.exception.MyException;<br>import com.ge.service.GeFileService;<br>import org.slf4j.Logger;<br>import org.slf4j.LoggerFactory;<br>import org.springframework.stereotype.Service;<br>import pdf.HtmlToPdfUtil;<br>import utils.Response;<br>import utils.ResultHttpStatus;<br><br>import javax.servlet.http.HttpServletResponse;<br>import java.io.*;<br>import java.util.UUID;<br><br>import static utils.PropertiesUtil.getProperty;<br><br>/**<br> * @Author: gaofeng_peng<br> * @Date: 2018/6/13 13:47<br> */<br>@Service("GeFileService")<br>public class FileServiceImpl implements GeFileService {<br>    private Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);<br><br>    public static String pdfLocationPath;<br><br>    public static String waterPdfLocationPath;<br><br>    public static  String systemOs;<br>    /**<br>     * 根据服务器系统设置pdf文件路径<br>     */<br>    static {<br>        systemOs = System.getProperty("os.name");<br>        if (systemOs.toLowerCase().startsWith("win")) {<br>            pdfLocationPath = getProperty("pdfLocationPath");<br>            waterPdfLocationPath = getProperty("waterPdfLocationPath");<br>        } else {<br>            pdfLocationPath = getProperty("pdfLocationPathForLinux");<br>            waterPdfLocationPath = getProperty("waterPdfLocationPathForLinux");<br>        }<br>    }<br><br>  <br>  <br>    @Override<br>    public Response htmlToPdf(HttpServletResponse res, String httpUrl, String downName) {<br>        Response response = new Response(ResultHttpStatus.OK.getValue(), ResultHttpStatus.OK.getName());<br>        //判断路径是否存在,不存在则创建<br>        File fileDir = new File(pdfLocationPath);<br>        File waterFileDir = new File(waterPdfLocationPath);<br>        if (!fileDir.exists()) {<br>            fileDir.setWritable(true);<br>            fileDir.mkdirs();<br>        }<br>        if (!waterFileDir.exists()) {<br>            waterFileDir.setWritable(true);<br>            waterFileDir.mkdirs();<br>        }<br>        String fileName = UUID.randomUUID().toString() + ".pdf";<br>        String source = pdfLocationPath + fileName;<br>        String outPath = waterPdfLocationPath + fileName;<br>        try {<br>            //调用HtmlToPdfUtilz中的convent的方法使html生成pdf<br>            boolean isSuccess = HtmlToPdfUtil.convert(httpUrl, source,systemOs);<br>            if (isSuccess) {<br>                //水印加密<br>                //  HtmlToPdfUtil.setWaterMarkForPDF(source,outPath,"");<br>                download(res, downName, fileName);<br>            } else {<br>                throw new MyException("pdf下载异常");<br>            }<br><br>        } catch (Exception e) {<br>            response.setMsg(e.getMessage());<br>            response.setStatus(ResultHttpStatus.INTERNAL_ERROR.getValue());<br>        }<br>        return response;<br>    }<br><br>
/**<br> * 响应下载<br> * @param resp<br> * @param downloadName 下载的pdf文件名<br> * @param fileName 系统中真正的文件名<br> */
public void download(HttpServletResponse resp, String downloadName, String fileName) {<br>        try {<br>            downloadName = new String(downloadName.getBytes("GBK"), "ISO-8859-1");<br>        } catch (UnsupportedEncodingException e) {<br>            e.printStackTrace();<br>        }<br>        String realPath = pdfLocationPath;<br>        String path = realPath + fileName;<br>        File file = new File(path);<br>        resp.reset();<br>        resp.setContentType("application/octet-stream");<br>        resp.setCharacterEncoding("utf-8");<br>        resp.setContentLength((int) file.length());<br>        resp.setHeader("Content-Disposition", "attachment;filename=" + downloadName + ".pdf");<br>        byte[] buff = new byte[1024];<br>        BufferedInputStream bis = null;<br>        OutputStream os = null;<br>        try {<br>            os = resp.getOutputStream();<br>            bis = new BufferedInputStream(new FileInputStream(file));<br>            int i = 0;<br>            while ((i = bis.read(buff)) != -1) {<br>                os.write(buff, 0, i);<br>                os.flush();<br>            }<br>        } catch (IOException e) {<br>            e.printStackTrace();<br>        } finally {<br>            try {<br>                bis.close();<br>                //完成后删除本地pdf文件<br>                file.delete();<br>            } catch (IOException e) {<br>                e.printStackTrace();<br>            }<br>        }<br><br>    }<br>}
 

HtmlToPdfUtil类

package pdf;


import com.itextpdf.text.BaseColor;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import org.apache.commons.lang3.StringUtils;
import utils.PropertiesUtil;

import java.io.FileOutputStream;

/**
 * @Author: gaofeng_peng
 * @Date: 2018/7/5 13:13
 */
public class HtmlToPdfUtil {
    //wkhtmltopdf在系统中的路径
    // private static final String toPdfTool = "D:\\wkhtmltox\\bin\\wkhtmltopdf.exe";


    /**
     * html转pdf
     *
     * @param srcPath    html路径
     * @param pdfLocationPath pdf保存路径
     * @return 转换成功返回true
     */
    public static boolean convert(String srcPath, String pdfLocationPath, String systemOs) throws Exception {
        String toPdfTool="";
        if (systemOs.toLowerCase().startsWith("win")) {
             toPdfTool = HtmlToPdfUtil.class.getClassLoader().getResource("wkhtmltox/bin/wkhtmltopdf.exe").getPath();
        }else {
            toPdfTool= PropertiesUtil.getProperty("wkhtmltopdfPath");
        }
        StringBuilder cmd = new StringBuilder();
        cmd.append(toPdfTool);
        cmd.append(" ");
        cmd.append("  --background");
        cmd.append(" --debug-javascript");
        cmd.append("  --header-line");//页眉下面的线
        cmd.append(" --header-spacing 10 ");//    (设置页眉和内容的距离,默认0)

        cmd.append(srcPath);
        cmd.append(" ");
        cmd.append(pdfLocationPath);

        boolean result = true;
        try {
            Process proc = Runtime.getRuntime().exec(cmd.toString());
            HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
            HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
            error.start();
            output.start();
            proc.waitFor();
        } catch (Exception e) {
            result = false;
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 生成水印,加密在这里实现
     * @param sourceFilePath    源文件路径
     * @param fileWaterMarkPath 水印生成文件路径
     * @throws Exception
     */
    public static void setWaterMarkForPDF(String sourceFilePath, String fileWaterMarkPath, String waterMarkName) throws Exception {
        //  String waterPath = Class.class.getClass().getResource("/1.png").getPath();
        PdfReader reader = new PdfReader(sourceFilePath);
        PdfStamper stamp = new PdfStamper(reader, new FileOutputStream(fileWaterMarkPath));

        int total = reader.getNumberOfPages() + 1;
        PdfContentByte under = null;
        //Image img = Image.getInstance(waterPath);
        //img.setAbsolutePosition(30, 100);//坐标
        // img.setRotation(-20);//旋转 弧度
        //img.setRotationDegrees(-35);//旋转 角度
        //img.scaleAbsolute(200,100);//自定义大小
        //img.scalePercent(100);//依照比例缩放
        BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA,
                BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
        if (StringUtils.isBlank(waterMarkName)) {
            waterMarkName = "Tech Mahindra";
        }

        int j = waterMarkName.length();
        char c = 0;
        int rise = 0;
        for (int i = 1; i < total; i++)  // 每一页都加水印
        {
            rise = 500;
            under = stamp.getUnderContent(i);
            // 添加图片
            // under.addImage(img);
            under.beginText();
            under.setColorFill(BaseColor.BLACK);
            under.setFontAndSize(bf, 30);
            // 设置水印文字字体倾斜 开始
            if (j >= 15) {
                under.setTextMatrix(200, 120);
                for (int k = 0; k < j; k++) {
                    under.setTextRise(rise);
                    c = waterMarkName.charAt(k);
                    under.showText(c + "");
                    rise -= 20;
                }
            } else {
                under.setTextMatrix(180, 100);
                for (int k = 0; k < j; k++) {
                    under.setTextRise(rise);
                    c = waterMarkName.charAt(k);
                    under.showText(c + "");
                    rise -= 18;
                }
            }
            // 字体设置结束
            under.endText();
        }

        stamp.close();
        reader.close();
    }
}
<strong>HtmlToPdfInterceptor类<br></strong>
package pdf;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
 * @Author: gaofeng_peng
 * @Date: 2018/7/5 13:17
 */
public class HtmlToPdfInterceptor extends Thread {
    private InputStream is;

    public HtmlToPdfInterceptor(InputStream is){
        this.is = is;
    }

    public void run(){
        try{
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line.toString()); //输出内容
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
<strong>水印部分的功能被我注释了,实际效果是可行的,只是本人设计的太丑,没脸见人所以注释。后期改良再说,有兴趣的小伙伴可以自己改良。至此pdf打印下载的功能就完成了。需要注意的是html最好用原生的,使用less预处理可能会导致样式的丢失。<br><br></strong>