前端打印pdf的方法

  • 内容
  • 评论
  • 相关

最近公司有项目需要用到前端打印pdf的功能,我网上找了几种方法,发现要不就是不合适,要不就是有缺陷(万恶的百度算法,导致打开的网页都是爬虫抄的),本人就在原有的基础上进行了优化和修改。

首先,打印pdf的方法总共有2类,1是直接用window.print(),2是用pdf插件。这里因为需求简单,我们就选择不引用pdf插件,直接用window.print()打印。

既然打印,我们肯定是需要局部打印,不能把打印按钮也打印进去,那么print方法的局部打印主要分3种方法(下面都是我网上抄的):

1.通过开始、结束标记(startprint、endprint)来打印

function doPrint() { 
    bdhtml=window.document.body.innerHTML; 
    sprnstr="<!--startprint-->"; //开始打印标识字符串有17个字符
    eprnstr="<!--endprint-->"; //结束打印标识字符串
    prnhtml=bdhtml.substr(bdhtml.indexOf(sprnstr)+17); //从开始打印标识之后的内容
    prnhtml=prnhtml.substring(0,prnhtml.indexOf(eprnstr)); //截取开始标识和结束标识之间的内容
    window.document.body.innerHTML=prnhtml; //把需要打印的指定内容赋给body.innerHTML
    window.print(); //调用浏览器的打印功能打印指定区域
    window.document.body.innerHTML=bdhtml;//重新给页面内容赋值;
    return false;
}

2.通过id选择器来替换内容打印,方法类似第一种

function doPrint2(){
    if(getExplorer() == "IE"){
        pagesetup_null();
    }
    //根据div标签ID拿到div中的局部内容
    bdhtml=window.document.body.innerHTML; 
    var jubuData = document.getElementById("printcontent").innerHTML;
    //把获取的 局部div内容赋给body标签, 相当于重置了 body里的内容
    window.document.body.innerHTML= jubuData; 
    //调用打印功能
    window.print();
    window.document.body.innerHTML=bdhtml;//重新给页面内容赋值;
    return false;
}
 
function pagesetup_null(){                
    var hkey_root,hkey_path,hkey_key;
    hkey_root="HKEY_CURRENT_USER";
    hkey_path="\\Software\\Microsoft\\Internet Explorer\\PageSetup\\";
    try{
        var RegWsh = new ActiveXObject("WScript.Shell");
        hkey_key="header";
        RegWsh.RegWrite(hkey_root+hkey_path+hkey_key,"");
        hkey_key="footer";
        RegWsh.RegWrite(hkey_root+hkey_path+hkey_key,"");
    }catch(e){}
}
 
function getExplorer() {
    var explorer = window.navigator.userAgent ;
    //ie 
    if (explorer.indexOf("MSIE") >= 0) {
        return "IE";
    }
    //firefox 
    else if (explorer.indexOf("Firefox") >= 0) {
        return "Firefox";
    }
    //Chrome
    else if(explorer.indexOf("Chrome") >= 0){
        return "Chrome";
    }
    //Opera
    else if(explorer.indexOf("Opera") >= 0){
        return "Opera";
    }
    //Safari
    else if(explorer.indexOf("Safari") >= 0){
        return "Safari";
    }
}

3.通过动态创建iframe来打印

//判断iframe是否存在,不存在则创建iframe
    var iframe=document.getElementById("print-iframe");
    if(!iframe){  
            var el = document.getElementById("printcontent");
            iframe = document.createElement('IFRAME');
            var doc = null;
            iframe.setAttribute("id", "print-iframe");
            iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;');
            document.body.appendChild(iframe);
            doc = iframe.contentWindow.document;
            //这里可以自定义样式
            doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题
            doc.write('<div>' + el.innerHTML + '</div>');
            doc.close();
            iframe.contentWindow.focus();            
    }
    setTimeout(function(){ iframe.contentWindow.print();},50)  //解决第一次样式不生效的问题
    if (navigator.userAgent.indexOf("MSIE") > 0){
        document.body.removeChild(iframe);
    }

这3种方法前2种,其实都是通过替换标签来解决的,但是这种方法有个弊端,就是现在页面会发生改变,而第3种不会出现这种情况,那么我们就用第3种。但是第三种方法网上贴出的代码也有问题,主要问题和解决方法如下:

1.css样式带不进去。

外部的css是带不进iframe里的,很多人因此选择直接用内联样式,或者直接iframe里也引用css,但是这样做又遇到一个问题,如果用内联会很丑,而且无法用sass和less,如果直接引用css,直接写html还好,如果用vue这种环境写,在本地环境下会直接放在style里,生产环境下会生成不知名的几个css,这个时候,我们就需要取出style和link:

doc = iframe.contentWindow.document;
let styles = document.getElementsByTagName('style');
for(let i = 0;i < styles.length;i++)
{
  doc.write(styles[i].outerHTML)
}
let links = document.getElementsByTagName('link');
for(let i = 0;i < links.length;i++)
{
  doc.write(links[i].outerHTML)
}

如果有deep样式,并且style标签用的是scoped,就需要额外获取data-v作为style的选择器,操作如下:

let data = document.getElementById("printContainer").dataset;
let dataV = "";
for(let key in data)
{
  if(key.indexOf("v-")!=-1)
  {
    dataV =  'data-' + key;
  }
}
doc.write(`<div ${dataV}>${_html}</div>`)

2.选择打印的dom的本身会丢失

这个就是乱copy别人代码的毛病了,人家用innterHTML,你就照抄,实际上应该使用outerHTML。

doc.write('<div>' + el.innerHTML + '</div>');
doc.write('<div>' + el.outerHTML+ '</div>');
//就这么简单

3.因为父级选择器,style不起作用

用刚才上面1的方法style确实传进去了,但是因为父级选择器的关系,style还是会不生效,办法也很简单,我们手动给iframe内部加同名父级。

let _html = el.outerHTML;
for(let i = 0;i < parentClassNameList.length;i++)
{
  _html = '<div class="' + parentClassNameList[i] + '">' + _html + '</div>'
}
doc.write('<div>' + _html + '</div>');

parentClassNameList为父类名列表,一层一层上去。

4.canvas内部的像素不会被copy

这个就需要额外copy,这里我直接贴出copy代码

let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId);
let _currentCanvas = document.getElementById(_canvasId);
let _iframeCtx = _iframeCanvas.getContext("2d");
_iframeCtx.drawImage(_currentCanvas,0,0);

好了,最后我们把所有代码放一起封装一下。

print.js:

const print = {
  isPrint:false,
  //styles:iframe的style于主页面的是隔离的,需要额外传输
  //otherStyle:额外的style,主要用于控制默认打印大小(如:otherStyle:'transform:scale(1.5);transform-origin: top left;',)
  //parentClassNameList:由于style可能会带父级的选择器,所以这里也需要模拟父级(由内向外,从domID本身开始算)
  //dataV:css scoped后需要添加的选择器,一般使用了deep修改css,都需要使用这个参数
  start:(props)=>{
    if(print.isPrint) return;
    print.isPrint = true;
    console.log("print")
    let domID = props.domID;
    let styles = props.styles;
    let links = props.links;
    let otherStyle = props.otherStyle;
    let parentClassNameList = props.parentClassNameList;
    let dataV = props.dataV;
    let delay = 500;
    if(props.delay) delay = props.delay;
    if(!dataV) dataV = "";
    let beforePrint = props.beforePrint;
    let printComplete = props.printComplete;
    let el = document.getElementById(domID);
    let iframe=document.getElementById("print-iframe");
    if(!iframe){
      iframe = document.createElement('IFRAME');
      iframe.setAttribute("id", "print-iframe");
      iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;z-index:1000');
      // iframe.setAttribute('style', 'position:absolute;width:1920px;height:1080px;left:0px;top:0px;z-index:1000');
      document.body.appendChild(iframe);
    }
    let doc = iframe.contentWindow.document;
    if(styles)
    {
      for(let i = 0;i < styles.length;i++)
      {
        // doc.write(`<style> ${styles[i].innerHTML} </style>`)
        doc.write(styles[i].outerHTML)
      }
    }
    if(links)
    {
      for(let i = 0;i < links.length;i++)
      {
        doc.write(links[i].outerHTML)
      }
    }
    doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题
    if(parentClassNameList)
    {
      let _html = el.outerHTML;
      for(let i = 0;i < parentClassNameList.length;i++)
      {
        _html = `<div class="${parentClassNameList[i]}">${_html}</div>`
      }
      doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" ${dataV}>${_html}</div>`)
    }
    else
    {
      doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" style="" ${dataV}>${el.outerHTML}</div>`)
    }
    doc.close();
    if(otherStyle)
    {
      let newEl =  iframe.contentWindow.document.getElementById(domID);
      newEl.setAttribute('style', otherStyle);
    }
    iframe.contentWindow.focus();
    if(beforePrint)
    {
      beforePrint(iframe)
    }
    setTimeout(function(){
      let _title = document.title;
      document.title = "布匹检验报告" + title;
      iframe.contentWindow.print();
      document.title = _title;
      document.body.removeChild(iframe);
      if(printComplete)
      {
        printComplete(iframe)
      }
      print.isPrint = false;
    },delay)  //解决第一次样式不生效和图片还没加载出来的问题
  },
  startPages:(props)=>{
    if(print.isPrint) return;
    print.isPrint = true;
    console.log("print")
    let list = props.list;
    let styles = props.styles;
    let links = props.links;
    let title = props.title;
    let delay = 500;
    if(props.delay) delay = props.delay;
    let pageHeight = props.pageHeight;
    let beforePrint = props.beforePrint;
    let printComplete = props.printComplete;
    let iframe=document.getElementById("print-iframe");
    if(!iframe){
      iframe = document.createElement('IFRAME');
      iframe.setAttribute("id", "print-iframe");
      iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;z-index:1000');
      // iframe.setAttribute('style', 'position:absolute;width:1920px;height:1080px;left:0px;top:0px;z-index:1000');
      document.body.appendChild(iframe);
    }
    let doc = iframe.contentWindow.document;
    if(styles)
    {
      for(let i = 0;i < styles.length;i++)
      {
        // doc.write(`<style> ${styles[i].innerHTML} </style>`)
        doc.write(styles[i].outerHTML)
      }
    }
    if(links)
    {
      for(let i = 0;i < links.length;i++)
      {
        doc.write(links[i].outerHTML)
      }
    }
    doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题
    list.forEach((item,index)=>{
      let domID = item.domID;
      let otherStyle = item.otherStyle;
      let parentClassNameList = item.parentClassNameList;
      let dataV = item.dataV;
      if(!dataV) dataV = "";
      let el = document.getElementById(domID);
      if(parentClassNameList)
      {
        let _html = el.outerHTML;
        for(let i = 0;i < parentClassNameList.length;i++)
        {
          _html = `<div class="${parentClassNameList[i]}">${_html}</div>`
        }
        doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" ${dataV}>${_html}</div>`)
      }
      else
      {
        doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" style="" ${dataV}>${el.outerHTML}</div>`)
      }
      if(otherStyle)
      {
        let newEl =  iframe.contentWindow.document.getElementById(domID);
        newEl.setAttribute('style', otherStyle);
      }
    })
    doc.close();
    iframe.contentWindow.focus();
    if(beforePrint)
    {
      beforePrint(iframe)
    }
    setTimeout(function(){
      let _title = document.title;
      document.title = "布匹检验报告" + title;
      iframe.contentWindow.print();
      document.title = _title;
      document.body.removeChild(iframe);
      if(printComplete)
      {
        printComplete(iframe)
      }
      print.isPrint = false;
    },delay)
  }

}
export default print

可以看到,我这里还加了多页面打印的版本。

使用:

let styles = document.getElementsByTagName('style');
print.start({
  domID:'testPrint',//需要打印的domID
  styles:styles,
  parentClassNameList:['dashboard','main-content','page'],//需要打印的dom的上层class
  beforePrint:(iframe)=>{
    //如果有canvas打印需求,打开下面代码
    // let _canvasId = "canvas";//canvas的id
    // let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId);
    // let _currentCanvas = document.getElementById(_canvasId);
    // let _iframeCtx = _iframeCanvas.getContext("2d");
    // _iframeCtx.drawImage(_currentCanvas,0,0);
  }
})
let styles = document.getElementsByTagName('style');
let links = document.getElementsByTagName('link');
let data = document.getElementById("printContainer").dataset;
let dataV = "";
for(let key in data)
{
  if(key.indexOf("v-")!=-1)
  {
    dataV =  'data-' + key;
  }
}
let print1 = document.getElementById('print1');
print.startPages({
  styles:styles,
  links:links,
  list:[
    {
      domID:"print1",
      //这里的额外样式是用来缩放页面,使页面刚好撑开到A4纸的大小
      otherStyle:`height:${print1.offsetHeight*1.82};transform:scale(${1.82});transform-origin: top left;`,
      parentClassNameList:['real-page','print-page','summary','his-right','his-container','comp'],
      dataV:dataV,
    },
    {
      domID:"print2",
      parentClassNameList:['real-page','print-page','details','his-right','his-container','comp'],
      dataV:dataV,
    }
  ],
  beforePrint:(iframe)=>{
    let _canvasId = olaHisMap.value.canvasID;
    let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId);
    let _currentCanvas = document.getElementById(_canvasId);
    let _iframeCtx = _iframeCanvas.getContext("2d");
    _iframeCtx.drawImage(_currentCanvas,0,0);
  }
})

注意:因为缩放功能是用scale实现的,多页面会造成排版遮盖,所以额外样式需要垫高页面。

最后说一下,这个做法vue和react都适用,但是ID需要自己额外赋一下。

1542183776360437.jpg    

20230217170221.png

评论

0条评论

发表评论

电子邮件地址不会被公开。