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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM