WKHtmltoPdf

Posted 杨帆的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WKHtmltoPdf相关的知识,希望对你有一定的参考价值。

踩过的坑

请一定要使用下面的这种方式获取系统的可执行命令,否则会报一堆的找不到目录等错误!!!

 String osname = System.getProperty("os.name").toLowerCase();
 String cmd = osname.contains("windows") ? "where wkhtmltopdf" : "which wkhtmltopdf";
 p = Runtime.getRuntime().exec(cmd);

由于wkhtmltoPdf是基于操作系统层面的pdf转换,因此,程序想获得Html转换pdf就需要经历四次IO操作,如果pdf的大小大于3M时,就会变得缓慢,建议考虑使用itext5进行pdf转换。下面是itext4、itext5和wkhtmltoPdf之间的耗时对比;

 

原理

1、wkhtmltopdf是一个独立安装、通过命令行交互、开源免费的将html内容转为pdf或图片的工具,命令行交互意味着只要能够调用本地命令(cmd或shell等)的开发语言均可使用,比如Java。其本质是使用内置浏览器内核渲染目标网页,然后再将网页渲染结果转换为PDF文档或图片。wkhtmltopdf官网地址:wkhtmltopdf,选择合适的系统版本安装即可。

2、创建待转换的目标HTML页面,可用任何熟悉的技术栈,要注意的一点是尽量保存页面为静态,尽量减少动态效果、交互。wkhtmltopdf也可支持直接转换html文件,不过还是建议以url方式来转换,更简便。

3、 部署运行html web服务,切换到bin目录,运行命令行进行转换:

/wkhtmltopdf http://yourdomain/target.html SAVE_PATH/target.pdf

4、命令结构:wkhtmltopdf [GLOBAL OPTION]... [OBJECT]... <output file>

在命令行上可通过 wkhtmltopdf –H 来查看所有的配置说明。官网文档:https://wkhtmltopdf.org/usage/wkhtmltopdf.txt

JAVA调用

1、首先需要封装命令参数

private static String buildCmdParam(String srcAbsolutePath, String destAbsolutePath, Integer pageHeight, Integer pageWidth) 
        StringBuilder cmd = new StringBuilder();
        cmd.append(findExecutable()).append(space)
                .append("--margin-left").append(space)
                .append("0").append(space)
                .append("--margin-right").append(space)
                .append("0").append(space)
                .append("--margin-top").append(space)
                .append("0").append(space)
                .append("--margin-bottom").append(space)
                .append("0").append(space)
                .append("--page-height").append(space)
                .append(pageHeight).append(space)
                .append("--page-width").append(space)
                .append(pageWidth).append(space)

                .append(srcAbsolutePath).append(space)
//                .append("--footer-center").append(space)
//                .append("[page]").append(space)
//                .append("--footer-font-size").append(space)
//                .append("14").append(space)
//
//                .append("--disable-smart-shrinking").append(space)
//                .append("--load-media-error-handling").append(space)
//                .append("ignore").append(space)
//                .append("--load-error-handling").append(space)
//                .append("ignore").append(space)
//                .append("--footer-right").append(space)
//                .append("WanG提供技术支持").append(space)
                .append(destAbsolutePath);
        return cmd.toString();
    
    /**
     * 获取当前系统的可执行命令
     *
     * @return
     */
    public static String findExecutable() 
        Process p;
        try 
            String osname = System.getProperty("os.name").toLowerCase();
            String cmd = osname.contains("windows") ? "where wkhtmltopdf" : "which wkhtmltopdf";
            p = Runtime.getRuntime().exec(cmd);
            new Thread(new ProcessStreamHandler(p.getErrorStream())).start();
            p.waitFor();
            return IOUtils.toString(p.getInputStream(), Charset.defaultCharset());
         catch (IOException e) 
            log.info("根据当前系统返回 wkhtmltopdf 执行命令失败,IO异常:", e);
         catch (InterruptedException e) 
            log.info("根据当前系统返回 wkhtmltopdf 执行命令失败,中断异常:", e);
        
        return "";
    

2、获取当前系统的命令参数

Process proc = Runtime.getRuntime().exec(finalCmd);

3、等待程序执行结果,并以ByteArrayOutputStream形式返回,最后在finally里面删除由于工具转换过程中生成的临时文件

private static ByteArrayOutputStream doProcess(String finalCmd, File htmlTempFile, File wkpdfDestTempFile) 
        InputStream is = null;
        try 
            Process proc = Runtime.getRuntime().exec(finalCmd);
            new Thread(new ProcessStreamHandler(proc.getInputStream())).start();
            new Thread(new ProcessStreamHandler(proc.getErrorStream())).start();

            proc.waitFor();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            is = new FileInputStream(wkpdfDestTempFile);
            byte[] buf = new byte[1024];

            while (is.read(buf, 0, buf.length) != -1) 
                baos.write(buf, 0, buf.length);
            

            return baos;
         catch (IOException | InterruptedException e) 
            log.error("html转换pdf出错", e);
            throw new RuntimeException("html转换pdf出错了");
         finally 
            if (htmlTempFile != null) 
                boolean delete = htmlTempFile.delete();
            
            if (wkpdfDestTempFile != null) 
                boolean delete = wkpdfDestTempFile.delete();
            
            if (is != null) 
                try 
                    is.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
    

完整代码如下

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Random;

@Slf4j
public class WkHtmltoxPdf 
    //空格
    private static final String space = " ";

    //文件前缀
    private static final String PREFIX = "tempFile";

    //文件后缀-html
    private static final String SUFIX_HTML = ".html";
    //文件后缀pdf
    private static final String SUFIX_PDF = ".pdf";

    private static final Random RANDOM = new Random(100);

    private static String FILEDIR_PATH = "/Users/yangfan/tools/wkhtmltox";

    private static final Integer PAGE_HEIGHT = 60;

    private static final Integer PAGE_WIDTH = 100;

    public static void main(String[] args) 
        testWkPdf(getHtml(), PAGE_HEIGHT, PAGE_WIDTH);
    

    public static void testWkPdf(String html, Integer pageHeight, Integer pageWidth) 
        byte[] bytes = html2pdf(html, pageHeight, pageWidth).toByteArray();
        storagePdf(bytes, FILEDIR_PATH, RANDOM.nextInt() + SUFIX_PDF);
    

    /**
     * 存储pdf文件
     *
     * @param bfile    pdf字节流
     * @param filePath 文件路径
     * @param fileName 文件名称
     */
    public static void storagePdf(byte[] bfile, String filePath, String fileName) 
        BufferedOutputStream bos = null;
        FileOutputStream fos = null;
        File file = null;
        try 
            File dir = new File(filePath);
            if ((!dir.exists()) && (dir.isDirectory())) 
                boolean mkdirs = dir.mkdirs();
            
            file = new File(filePath + "/" + fileName);
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos);
            bos.write(bfile);
         catch (Exception e) 
            e.printStackTrace();
         finally 
            if (bos != null) 
                try 
                    bos.close();
                 catch (IOException e1) 
                    e1.printStackTrace();
                
            
            if (fos != null) 
                try 
                    fos.close();
                 catch (IOException e1) 
                    e1.printStackTrace();
                
            
        
    

    /**
     * 将传入的页面转换成pdf,返回pdf字节数组
     * 默认自动随机生成文件名称
     *
     * @param html html页面信息
     * @return byte[] pdf字节流
     */
    public static ByteArrayOutputStream html2pdf(String html, Integer pageHeight, Integer pageWidth) 
        String fileName = System.currentTimeMillis() + RANDOM.nextInt() + "";
        String dest = FILEDIR_PATH;
        return doHtml2pdf(html, dest, pageHeight, pageWidth);
    

    private static ByteArrayOutputStream doHtml2pdf(String html, String dest, Integer pageHeight, Integer pageWidth) 
        String wkhtmltopdf = findExecutable();
        //将内存中的html文件存储到一个临时地方
        File htmlTempFile = createFile(PREFIX, SUFIX_HTML, dest);
        FileUtil.writeString(html, htmlTempFile, CharsetUtil.UTF_8);

        //wk转换pdf之后的pdf存储文件地址
        File wkpdfDestTempFile = createFile(PREFIX, SUFIX_PDF, dest);

        if (StrUtil.isBlank(wkhtmltopdf)) 
            log.info("no wkhtmltopdf found!");
            throw new RuntimeException("html转换pdf出错了,未找到wkHtml工具");
        

        String srcAbsolutePath = htmlTempFile.getAbsolutePath();
        String destAbsolutePath = wkpdfDestTempFile.getAbsolutePath();

        File parent = wkpdfDestTempFile.getParentFile();
        if (!parent.exists()) 
            boolean dirsCreation = parent.mkdirs();
            log.info("create dir for new file,", dirsCreation);
        

        String finalCmd = buildCmdParam(srcAbsolutePath, destAbsolutePath, pageHeight, pageWidth);

        return doProcess(finalCmd, htmlTempFile, wkpdfDestTempFile);

    

    /**
     * 执行wkHtmltox命令,读取生成的的pdf文件,输出执行结果,最后删除由于执行wk命令生成的零时的pdf文件和html文件
     *
     * @param finalCmd          cmd命令
     * @param htmlTempFile      html零时文件
     * @param wkpdfDestTempFile 生成的pdf文件
     * @return byte[] pdf字节流
     */
    private static ByteArrayOutputStream doProcess(String finalCmd, File htmlTempFile, File wkpdfDestTempFile) 
        InputStream is = null;
        try 
            Process proc = Runtime.getRuntime().exec(finalCmd);
            new Thread(new ProcessStreamHandler(proc.getInputStream())).start();
            new Thread(new ProcessStreamHandler(proc.getErrorStream())).start();

            proc.waitFor();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            is = new FileInputStream(wkpdfDestTempFile);
            byte[] buf = new byte[1024];

            while (is.read(buf, 0, buf.length) != -1) 
                baos.write(buf, 0, buf.length);
            

            return baos;
         catch (IOException | InterruptedException e) 
            log.error("html转换pdf出错", e);
            throw new RuntimeException("html转换pdf出错了");
         finally 
            if (htmlTempFile != null) 
                boolean delete = htmlTempFile.delete();
            
            if (wkpdfDestTempFile != null) 
                boolean delete = wkpdfDestTempFile.delete();
            
            if (is != null) 
                try 
                    is.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
    

    public static File createFile(String prefix, String sufix, String fileDirPath) 
        File file = null;
        File fileDir = new File(fileDirPath);
        try 
            file = File.createTempFile(prefix, sufix, fileDir);
         catch (IOException e) 
            log.info("创建文件失败:", e.getCause());
        
        return file;
    

    private static String buildCmdParam(String srcAbsolutePath, String destAbsolutePath, Integer pageHeight, Integer pageWidth) 
        StringBuilder cmd = new StringBuilder();
        cmd.append(findExecutable()).append(space)
                .append("--margin-left").append(space)
                .append("0").append(space)
                .append("--margin-right").append(space)
                .append("0").append(space)
                .append("--margin-top").append(space)
                .append("0").append(space)
                .append("--margin-bottom").append(space)
                .append("0").append(space)
                .append("--page-height").append(space)
                .append(pageHeight).append(space)
                .append("--page-width").append(space)
                .append(pageWidth).append(space)

                .append(srcAbsolutePath).append(space)
//                .append("--footer-center").append(space)
//                .append("[page]").append(space)
//                .append("--footer-font-size").append(space)
//                .append("14").append(space)
//
//                .append("--disable-smart-shrinking").append(space)
//                .append("--load-media-error-handling").append(space)
//                .append("ignore").append(space)
//                .append("--load-error-handling").append(space)
//                .append("ignore").append(space)
//                .append("--footer-right").append(space)
//                .append("WanG提供技术支持").append(space)
                .append(destAbsolutePath);
        return cmd.toString();
    

    /**
     * 获取当前系统的可执行命令
     *
     * @return
     */
    public static String findExecutable() 
        Process p;
        try 
            String osname = System.getProperty("os.name").toLowerCase();
            String cmd = osname.contains("windows") ? "where wkhtmltopdf" : "which wkhtmltopdf";
            p = Runtime.getRuntime().exec(cmd);
            new Thread(new ProcessStreamHandler(p.getErrorStream())).start();
            p.waitFor();
            return IOUtils.toString(p.getInputStream(), Charset.defaultCharset());
         catch (IOException e) 
            log.info("根据当前系统返回 wkhtmltopdf 执行命令失败,IO异常:", e);
         catch (InterruptedException e) 
            log.info("根据当前系统返回 wkhtmltopdf 执行命令失败,中断异常:", e);
        
        return "";
    

    private static class ProcessStreamHandler implements Runnable 
        private InputStream is;

        public ProcessStreamHandler(InputStream is) 
            this.is = is;
        

        @Override
        public void run() 
            BufferedReader reader = null;
            try 
                InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
                reader = new BufferedReader(isr);
                String line;
                while ((line = reader.readLine()) != null) 
                    log.debug("---++++++++++--->" + line);
                
             catch (IOException e) 
                e.printStackTrace();
             finally 
                if (reader != null) 
                    try 
                        reader.close();
                     catch (IOException e) 
                        e.printStackTrace();
                    
                
            
        
    

wkhtmltopdf/perl:HTTP 标头和日志记录

【中文标题】wkhtmltopdf/perl:HTTP 标头和日志记录【英文标题】:wkhtmltopdf/perl: HTTP headers & logging 【发布时间】:2011-04-06 20:57:30 【问题描述】:

我刚刚发现了 wkhtmltopdf,我正在尝试在 perl CGI 脚本中使用它来生成 PDF。基本上,perl 脚本编写一个 HTML 文件,通过 system() 调用 wkhtmltopdf 创建一个 pdf,然后下载 pdf 并删除临时文件。

open NNN, ">$path_to_files/$file_pdf.html" or die "can't write file: $!";
print NNN $text;
close NNN;

my @pdfSettings = (
    "d:/very/long/path/wkhtmltopdf",
    "$path_to_files/$file_pdf.html",
    "$path_to_files/$file.pdf"
    );

system(@pdfSettings);

open(DLFILE, '<', "$path_to_files/$file.pdf");
   print $q->header(
        -type=> 'application/x-download',
        -attachment => "$file.pdf",
        -filename => "$file.pdf",
        'Content-length' => -s "$path_to_files/$file.pdf",
);

binmode DLFILE;
print while <DLFILE>;
close (DLFILE);


unlink("$path_to_files/$file_pdf.html");
unlink("$path_to_files/$file.pdf");

这在我的本地服务器上运行良好。但是,当我将它上传到我的公共服务器时,它会创建 pdf 文件,然后因“指定的 CGI 应用程序因未返回一组完整的 HTTP 标头而行为异常”而死掉。

将“print $q->header”移动到 system() 调用之前会导致 pdf 在文件顶部生成 wkhtmltopdf 的控制台输出(“Loading pages (1/6)”等),所以我认为正在发生的事情是 wkhtmltopdf 正在向服务器发送无标题的信息并导致它失败。但是我在 wkhtmltopdf 文档中找不到任何选项来关闭控制台输出,而且我无法找到一种 perl 方法来抑制/重定向该输出。

(是的,我知道 WKHTMLTOPDF.pm,但我在安装它时遇到了我的 ActivePerl 风格的问题,我想尽可能避免切换。)

【问题讨论】:

【参考方案1】:

如何通过 qx 或反引号而不是 system() 执行,并将输出重定向到 NUL:?


qx("d:/very/long/path/wkhtmltopdf" "$path_to_files/$file_pdf.html" "$path_to_files/$file.pdf" > NUL: 2> NUL:);

【讨论】:

有效!我试过 qx 但不知道 NUL。谢谢! 等等。 . .抱歉,它毕竟不起作用。有一次我试图检查输出,所以我注释掉了删除临时 pdf 的行,所以它一次又一次地下载。但是现在我已经删除了该文件,它根本无法创建 pdf,只显示以下消息:“Content-Disposition: attachment; filename="Advanced.pdf" Filename: Advanced.pdf Content-length Content-Type: application/x-download " ("Advanced" 是 $file 的值) 那是在私人服务器上。公共服务器失败并显示与以前相同的 HTTP 标头消息。 也许您正试图在您无权写入文件的地方生成文件? 盖克。如果有人用评论回复答案,我希望 *** 有一个通知选项。

以上是关于WKHtmltoPdf的主要内容,如果未能解决你的问题,请参考以下文章

前端大纲********

前端HTML之页面结构

编程小白,如何区分HTML5开发和前端开发?

前端基础(HTML,CSS,JavaScript)知识笔记,附:前端基础面试题!!

html前端代码

python--初识html前端