• HTML转为PDF,图片导出失败的终极解决方案


    如题项目有需求将一个页面导出为pdf,然而页面中的图片却始终无法导出成功

    一、导出的方法

    查询了许多大佬的帖子,找到了如下导出的方法

      const PdfDownload = function(domId) {
          var targetDom = $('#'+domId)
          // 把需要导出的pdf内容clone一份,这样对它进行转换、微调等操作时才不会影响原来界面
          var copyDom = targetDom.clone()
          // 新的div宽高跟原来一样,高度设置成自适应,这样才能完整显示节点中的所有内容(比如说表格滚动条中的内容)
          copyDom.width(targetDom.width() + 'px')
          copyDom.height(targetDom.height()+200 + 'px')
    
          $('body').append(copyDom)// ps:这里一定要先把copyDom append到body下,然后再进行后续的glyphicons2canvas处理,不然会导致图标为空
    
        // svg2canvas(copyDom)
        // loadImg(copyDom)
        html2canvas(copyDom, {
        onrendered: function(canvas) {
          var imgData = canvas.toDataURL('image/jpeg')
          var img = new Image()
          img.src = imgData
          // 根据图片的尺寸设置pdf的规格,要在图片加载成功时执行,之所以要*0.225是因为比例问题
          img.onload = function() {
            // 此处需要注意,pdf横置和竖置两个属性,需要根据宽高的比例来调整,不然会出现显示不完全的问题
            if (this.width > this.height) {
              var doc = new jsPDF('l', 'mm', [this.width * 0.225, this.height * 0.225])
            } else {
              var doc = new jsPDF('p', 'mm', [this.width * 0.225, this.height * 0.225])
            }
            doc.addImage(imgData, 'jpeg', 0, 0, this.width * 0.225, this.height * 0.225)
            // 根据下载保存成不同的文件名
            doc.save('pdf_' + new Date().getTime() + '.pdf')
          }
          // 删除复制出来的div
          copyDom.remove()
        },
        background: '#FFF',
        // 这里给生成的图片默认背景,不然的话,如果你的html根节点没设置背景的话,会用黑色填充。
        allowTaint: true // 避免一些不识别的图片干扰,默认为false,遇到不识别的图片干扰则会停止处理html2canvas
      })
    }

    二、初步测试的结果

    有了上面的方法当然迫不及待的进行测试-- 测试导出页面如下
    需要导出的页面

    导出成功结果如下
    导出如图

    这一测试发现并没有得到自己期望的结果,页面大致导出成功了,可是页面原本的头像图片怎么就没导出来呢?



    三、使用f12查找原油

    打开浏览器使用另一个用户进行测试发现… 该用户没有上传头像,我默认加载了一张本地的图片作为用户默认头像
    使用本地地址的页面
    而加载为默认图片的页面使用jsPdf将其进行导出,这个头像图片就可以成功被下载下来
    图片为本地地址导出成功
    于是做出如下推测…
    查找原油

    经过这一测试初步断定是js 中同源策略所引起的跨域请求图片,所导致的jsPdf读取页面中图片失败的问题

    四、方案一

    到目前,问题虽然初步已锁定,但是还没有切实可行的解决方案,这咋办?

    首先想到:就是把图片从服务器下载到本地

    于是想到了使用nodeJS http+fs 从服务器将文件下载,然后将其写入到本地文件夹中
    参考 https://www.jianshu.com/p/28e3de79fd49

    var http = require(''http'),fs = require('fs');
    http.get(path,function(req,res){  //path为网络图片地址
      var imgData = '';
      req.setEncoding('binary');
      req.on('data',function(chunk){
        imgData += chunk
      })
      req.on('end',function(){
        fs.writeFile(path,imgData,'binary',function(err){  //path为本地路径例如public/logo.png
          if(err){console.log('保存出错!')}else{
            console.log('保存成功!')
          }
        })
      })
    })

    再重新 为节点添加一个img 标签,将其url指定为刚才下载的文件地址,在pdf 下载完成后再使用
    如下方法将其删除掉 参考 https://blog.csdn.net/dongmelon/article/details/102456717

    var fs = require('fs')
    /**
     * 
     * @param {*} path 必传参数可以是文件夹可以是文件
     * @param {*} reservePath 保存path目录 path值与reservePath值一样就保存
     */
    function delFile(path, reservePath) {
        if (fs.existsSync(path)) {
            if (fs.statSync(path).isDirectory()) {
                let files = fs.readdirSync(path);
                files.forEach((file, index) => {
                    let currentPath = path + "/" + file;
                    if (fs.statSync(currentPath).isDirectory()) {
                        delFile(currentPath, reservePath);
                    } else {
                        fs.unlinkSync(currentPath);
                    }
                });
                if (path != reservePath) {
                    fs.rmdirSync(path);
                }
            } else {
                fs.unlinkSync(path);
            }
        }
    
    }

    后来经测试,很显然这个想法很幼稚(浏览器如何使用nodeJS?), 最终测试这种方法是不可行的!

    五、方案二

    使用canvas 根据图片url重新将图片绘制然后进行下载
    参考https://www.jb51.net/article/128554.htm

    /**
     * 
     * 查询目标容器中images
     * 使用nodejs进行图片下载到本地
     * 增加一个image 使用本地src
     * @param targetElem  {Element object}
     */
    function loadImg(targetElem){
    
      var svgElem = targetElem.find('img')
      svgElem.each(function(index, node) {
        var parentNode = node.parentNode
             //通过构造函数来创建的 img 实例,在赋予 src 值后就会立刻下载图片,相比 createElement() 创建 <img> 省去了 append(),也就避免了文档冗余和污染
         var Img = new Image(),
          dataURL='';
         Img.src=url;
         Img.onload=function(){ //要先确保图片完整获取到,这是个异步事件
          var canvas = document.createElement("canvas"), //创建canvas元素
           width=Img.width, //确保canvas的尺寸和图片一样
           height=Img.height;
          canvas.width=width;
          canvas.height=height;
          canvas.getContext("2d").drawImage(Img,0,0,width,height); //将图片绘制到canvas中
          dataURL=canvas.toDataURL('image/jpeg'); //转换图片为dataURL
         };
         
         parentNode.appendChild(canvas)
      })
    }

    后来又遇到了Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

    根据资料https://blog.csdn.net/u013040887/article/details/78986598 在方法中新增node.setAttribute('crossOrigin', 'anonymous'); 遗憾的是最后并未成功解决问题,依然需要重新寻找新的解决方案…

    六、方案三

    然后想到了使用XMLHttpRequest先把图片下载回来再重新为img 赋值

    于是 增加如下方法使用 xmlHttpRequest进行图片下载

    function downloadByXmlhttprequest(imgDom,cb){
    
      var xhr = new XMLHttpRequest()
        
        xhr.onreadystatechange = function () {
          var blob
          if(xhr.readyState === 4){
        
            // 使用URL.createObjectURL将Blob对象转换为可访问的url地址
            var src = URL.createObjectURL(xhr.response)
            console.log(src)
            imgDom.src = src
            cb(src)
          }
        }
        
        xhr.open('GET',imgDom.src, true)
        // 设置响应数据格式为Blob对象
        xhr.responseType = 'blob'
        
        // 设置请求头
        xhr.setRequestHeader('X-Requested-With', 'OpenAPIRequest')
        
        xhr.send()
    }

    参考https://blog.csdn.net/weixin_34384915/article/details/91756646

    但是其中又遇到了请求未携带cookie而失败问题

    在这里插入图片描述

    后来参照 https://blog.csdn.net/u011674895/article/details/83932461解决token失效问题
    在这里插入图片描述
    在方法中添加如下代码
    xhr.withCredentials = true;

    最终经过如下多次测试,终于成功了
    在这里插入图片描述

    成功导出图片

    七、完整代码

    最后完成这些操作的完整代码(这里我是写了一个外部js)如下

    1、使用XMLHttpRequest进行图片二次下载
    /**
     * 使用XMLHttpRequest进行图片二次下载
     * @param imgDom {Objec}  target object
     * @param cb{Object}success callback
     */
    function downloadByXmlhttprequest(imgDom,cb){
    
      var xhr = new XMLHttpRequest()
        
        xhr.onreadystatechange = function () {
          if(xhr.readyState === 4){
        
            // 使用URL.createObjectURL将Blob对象转换为可访问的url地址
            var src = URL.createObjectURL(xhr.response)
            imgDom.src = src
            cb(src)
          }
        }
        
        xhr.open('GET',imgDom.src, true)
        xhr.withCredentials = true;
        // 设置响应数据格式为Blob对象
        xhr.responseType = 'blob'
        
        // 设置请求头
        xhr.setRequestHeader('X-Requested-With', 'OpenAPIRequest')
        
        xhr.send()
    }
    2、转换页面的图片
    function imgConvert(targetElem,cb){
      var svgElem = targetElem.find('img')
      svgElem.each(function(index, node) {
        var parentNode = node.parentNode
        downloadByXmlhttprequest(node,cb)
      })
    
    }
    3、html2canvas执行下载
    function executeDown(copyDom){
      html2canvas(copyDom, {
        onrendered: function(canvas) {
          var imgData = canvas.toDataURL('image/jpeg')
          var img = new Image()
          img.src = imgData
          // 根据图片的尺寸设置pdf的规格,要在图片加载成功时执行,之所以要*0.225是因为比例问题
          img.onload = function() {
            // 此处需要注意,pdf横置和竖置两个属性,需要根据宽高的比例来调整,不然会出现显示不完全的问题
            if (this.width > this.height) {
              var doc = new jsPDF('l', 'mm', [this.width * 0.225, this.height * 0.225])
            } else {
              var doc = new jsPDF('p', 'mm', [this.width * 0.225, this.height * 0.225])
            }
            doc.addImage(imgData, 'jpeg', 0, 0, this.width * 0.225, this.height * 0.225)
            // 根据下载保存成不同的文件名
            doc.save('pdf_' + new Date().getTime() + '.pdf')
          }
          // 删除复制出来的div
          copyDom.remove()
        },
        background: '#FFF',
        // 这里给生成的图片默认背景,不然的话,如果你的html根节点没设置背景的话,会用黑色填充。
        allowTaint: true // 避免一些不识别的图片干扰,默认为false,遇到不识别的图片干扰则会停止处理html2canvas
      })
    }
    4、供外部调用的导出方法
    const PdfDownload = function(domId) {
      var targetDom = $('#'+domId)
      // 把需要导出的pdf内容clone一份,这样对它进行转换、微调等操作时才不会影响原来界面
      var copyDom = targetDom.clone()
      // 新的div宽高跟原来一样,高度设置成自适应,这样才能完整显示节点中的所有内容(比如说表格滚动条中的内容)
      copyDom.width(targetDom.width() + 'px')
      copyDom.height(targetDom.height()+200 + 'px')
    
      $('body').append(copyDom)// ps:这里一定要先把copyDom append到body下,然后再进行后续的glyphicons2canvas处理,不然会导致图标为空
    
      // svg2canvas(copyDom)
      // loadImg(copyDom)
      imgConvert(copyDom,function(res){
        executeDown(copyDom)
      })
    
    }
    
    export { PdfDownload }

    最后这里使用到的 html2canvas-0.4.1 , jquery-2.1.4.min , jspdf.min 如下
    插件网盘https://pan.baidu.com/s/1MMNOjmU8H3ebmWdB5nBzqw 提取码 jg7q

  • 相关阅读:
    IDEA 2018 搭建 Spring MVC helloworld
    Intellij IDEA创建Spring的Hello World项目
    浅谈finally使用坑。
    ArrayList源码分析
    Spring依赖注入的四种方式(重点是注解方式)
    Spring常用的三种注入方式
    interview question
    待提高问题
    myeclipse svn 在线安装
    《SQL 进阶教程》 自连接排序
  • 原文地址:https://www.cnblogs.com/dengxiaoning/p/13336780.html
Copyright © 2020-2023  润新知