html界面转pdf技术研究

Posted by gjx on 2020-08-27 00:00:00

最近在做一个功能,需要将当前的web界面转换成pdf进行下载。

目前网上流传的主流缺乏有三种:

  • 用itextpdf进行转换
  • 用jspdf进行转换
  • 用wkhtmltopdf进行转换

综合三种技术,jspdf直接被pass掉,因为他是纯前端利用canvas截图来切割图片来生成的pdf,也就是pdf的内容全部为图片,这样会存在页面放大模糊等弊端。

那么再考虑itextpdf,经过查找资料和测试,这个工具对css的支持和页面的渲染效果不是太好,在浏览器正常打开的界面在经过itextpdf转换后样式不一样,所以这个也被pass。

那么最后只剩下wkhtmltopdf,其实刚开始也不太想用这个插件,因为并非java开发,需要编译为linux或者windows上的可执行程序进行调用。而且在实际测试这个插件的时候,本来打算直接将页面地址传给wkhtmltopdf进行转换,奈何我的前端是由react开发,webpack编译,在转换的时候总是会报错,刚开始怀疑是兼容性问题,所以下载了一个wiki内核的浏览器,经过测试同样打不开我们的系统界面,各种尝试无果后,有于时间原因,决定放弃解决系统兼容性问题。

下面想到了一种办法,直接将当前打开的界面的dom树进行clone,然后将dom树对象转为html文本传给后台,由后台将这个临时的html页面传给wkhtmltopdf生成pdf,当然这中间涉及到了clone后的dom树中的canvas图形消失,样式等问题。

这里采用了,直接将当前dom树中的head标签的所有内容和所要导出的内容的dom树一起clone并转换为字符传给后台,这样就保证了head中的css也可以传给后台,当前这也是初步的一个想法,因为head中的样式其实在我们编译前端的时候就已经可以确定好的,所以完全可以自己在后台直接使用编译好的样式,而不用每次从前台传,暂且先这样吧。

那么接下来解决canvas内容丢失问题,因为原来的echarts图形是js代码动态给canvas绘制的,clone来的canvas对象肯定只是个空的canvas没有内容,那么我想到的办法就是遍历当前内容区域所有canvas标签,并且转换为图片替换到clone来的canvas标签,这样整个界面canvas就变为了图片,也可以传递给后台了。

大体思路就到这里。下面是一部分的代码:

 var getLayerNode = (node) => {
      if(node.classList && node.classList.contains("layer")){
        return node
      }else{
        return getLayerNode(node.parentElement)
      }
 } 
const exporToPdf = ()=>{
    var app = refContent.current
    var list = app.getElementsByTagName("canvas")

    var map={}
    var promisArray=[]

    var getCanvasImg = (id, canvasItem) => {
      return new Promise(function (resolve, reject) {
        var promise = html2canvas(canvasItem)
        promise.then(canvas => {
          var image = new Image();
          image.src = canvas.toDataURL("image/png", 1);
          map[id] = image
          document.getElementById(id).removeAttribute("id")
          resolve()
        })
      });
    }
    for (var i = 0; i < list.length; i++) {
      var canvasItem = list[i]
      if(canvasItem){
        var id = uuid(6)
        canvasItem.id = id
        promisArray.push(getCanvasImg(id, getLayerNode(canvasItem.parentElement)))
      }

    }
    var tmpApp = app.cloneNode(true);
    tmpApp.style.display = "none"
    Promise.all(promisArray).then(() => {
      tmpApp.id = "appTmp"
      document.body.appendChild(tmpApp)
      for (var id in map) {
        if (map[id]) {
          var canvas = document.getElementById(id)
          canvas.parentNode.replaceChild(map[id], canvas);
        }
      }
      dispatch(Actions.exportReportToPdf(document.head.innerHTML, tmpApp.outerHTML, tmpApp.style.width, tmpApp.style.height, id, name))
      document.body.removeChild(tmpApp)
    })

  }

@Slf4j
public class WkHtmlToPdfUtils {

    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) {
                    log.info(line.toString());
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private String wkHtmlToPdfName = "wkhtmltopdf";

    public WkHtmlToPdfUtils() {
        this.wkHtmlToPdfName = isWindows() ? "wkhtmltopdf.exe" : "wkhtmltopdf";
    }

    private static boolean isLinux() {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }

    private static boolean isWindows() {
        return System.getProperty("os.name").toLowerCase().contains("windows");
    }

    public String convertHtmlTextToPdf(ExportBean exportBean) {
        String tmpDir = System.getProperty("java.io.tmpdir");
        String fileName = UUID.randomUUID().toString();
        exportBean.setUuid(fileName);
        String resultPath = null;
        try {
            Document html = Jsoup.parse("<!DOCTYPE html><html></html>");
            html.head().append(exportBean.getHead());
            html.body().append(exportBean.getContent());

            Elements scripts = html.getElementsByTag("script");
            for(Element script : scripts) {
                script.remove();
            }
            Element app = html.getElementById("appTmp");
            String style = app.attr("style");
            app.attr("style", style.replace("display: none;", ""));
            resultPath = writeHtmlToFile(tmpDir,fileName+".html",html.toString());
        } catch (Exception e) {
            log.error("创建html文件失败",e);
            return null;
        }
        if(resultPath != null) {
            String targetPath = tmpDir+File.separator+fileName+".pdf";
            if(convertHtmlToPdf(resultPath, targetPath, reportView)) {
                reportView.setFilePath(targetPath);
                log.info("生成PDF文件:{}",targetPath);
                return targetPath;
            }

        }

        return null;
    }

    public boolean convertHtmlToPdf(String srcPath, String destPath, ExportBean exportBean) {
        File file = new File(destPath);
        File parent = file.getParentFile();
        if (!parent.exists()) {
            parent.mkdirs();
        }
        StringBuilder cmd = new StringBuilder();

        if(System.getenv("WKHTMLTOPDF_HOME") == null) {
            log.error("获取环境变量WKHTMLTOPDF_HOME失败");
            return false;
        }
        String pxWidth = exportBean.getWidth().replaceAll("px", "");
        String pxHeight = exportBean.getHeight().replaceAll("px", "");

        long width =  (long) Math.ceil((Long.parseLong(pxWidth)*25.4 / 96));
        long height = (long) Math.ceil((Long.parseLong(pxHeight)*25.4 / 96));

        String wkToPdfTool=System.getenv("WKHTMLTOPDF_HOME")+File.separator+"bin"+File.separator+wkHtmlToPdfName;
        cmd.append(wkToPdfTool).append(" -B 0 -L 0 -R 0 -T 0 --disable-smart-shrinking").append(" --page-width ").append(width).append(" --page-height ")
                .append(height).append(" ").append(srcPath).append(" ").append(destPath);

        log.info(cmd.toString());
        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();
        }
        new File(srcPath).deleteOnExit();
        return result;
    }

    private String writeHtmlToFile(String path, String name, String content) throws Exception {
        File pathFile = new File(path);
        if (!pathFile.exists()) {
            pathFile.mkdirs();
        }
        File file = new File(path + File.separator + name);
        file.createNewFile();
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        out.write(content);
        out.flush();
        out.close();
        return file.getPath();
    }
}