使用Javascript从HTML中的div生成PDF

349

I have the following html code:

<!DOCTYPE html>
<html>
    <body>
        <p>don't print this to pdf</p>
        <div id="pdf">
            <p><font size="3" color="red">print this to pdf</font></p>
        </div>
    </body>
</html>

我想做的就是将id为“pdf”的div中的内容打印成pdf格式。这必须使用JavaScript完成。然后,“pdf”文档应自动下载,文件名为“foobar.pdf”。
我一直在使用jspdf来完成这个任务,但它只有一个“text”函数,只接受字符串值。我想提交HTML给jspdf,而不是文本。

1
如上所述,我不想使用“text”函数。我想要使用HTML。您的链接仅处理纯文本而不是HTML。 - John Crawford
5
jsPDF确实有一个fromHTML函数;请参考"http://mrrio.github.io/jsPDF/"上的“HTML渲染器”示例。 - mg1075
18个回答

320

jsPDF可以使用插件。为了使其能够打印HTML,您需要包含某些插件并执行以下操作:

  1. 转到https://github.com/MrRio/jsPDF并下载最新版本。
  2. 在您的项目中包含以下脚本:
    • jspdf.js
    • jspdf.plugin.from_html.js
    • jspdf.plugin.split_text_to_size.js
    • jspdf.plugin.standard_fonts_metrics.js

如果您想忽略某些元素,则必须使用ID标记它们,然后在jsPDF的特殊元素处理程序中忽略它们。因此,您的HTML应如下所示:

<!DOCTYPE html>
<html>
  <body>
    <p id="ignorePDF">don't print this to pdf</p>
    <div>
      <p><font size="3" color="red">print this to pdf</font></p>
    </div>
  </body>
</html>

然后,您可以使用以下JavaScript代码在弹出窗口中打开创建的PDF文件:
var doc = new jsPDF();          
var elementHandler = {
  '#ignorePDF': function (element, renderer) {
    return true;
  }
};
var source = window.document.getElementsByTagName("body")[0];
doc.fromHTML(
    source,
    15,
    15,
    {
      'width': 180,'elementHandlers': elementHandler
    });

doc.output("dataurlnewwindow");

对我来说,这创造了一个漂亮整洁的PDF文件,其中只包含一行“将此打印到PDF”。

请注意,特殊元素处理程序仅处理当前版本中的ID,这也在GitHub问题中说明。它说明:

因为匹配是针对节点树中的每个元素进行的,所以我的愿望是使其尽可能快速。在这种情况下,它意味着“只匹配元素ID”。元素ID仍然以jQuery样式“#id”进行处理,但这并不意味着支持所有jQuery选择器。

因此,用类选择器(如“.ignorePDF”)替换“#ignorePDF”对我无效。相反,您必须为要忽略的每个元素添加相同的处理程序,例如:
var elementHandler = {
  '#ignoreElement': function (element, renderer) {
    return true;
  },
  '#anotherIdToBeIgnored': function (element, renderer) {
    return true;
  }
};

示例中可以看出,选择像 'a' 或 'li' 这样的标签是可能的。但对于大多数用例来说,这可能有点过于不受限制了:

我们支持特殊元素处理程序。使用 jQuery 风格的 ID 选择器进行注册,可用于 ID 或节点名称。(例如 "#iAmID"、"div"、"span" 等)。目前不支持任何其他类型的选择器(类或复合选择器)。

需要补充的一件非常重要的事情是,您将失去所有的样式信息(CSS)。幸运的是,jsPDF 能够很好地格式化 h1、h2、h3 等标题,这对我的目的已经足够了。此外,它只会打印文本节点中的文本,这意味着它不会打印文本区域等内容的值。例如:

<body>
  <ul>
    <!-- This is printed as the element contains a textnode -->        
    <li>Print me!</li>
  </ul>
  <div>
    <!-- This is not printed because jsPDF doesn't deal with the value attribute -->
    <input type="textarea" value="Please print me, too!">
  </div>
</body>

4
我猜元素处理程序可以是一个类?这更符合HTML5标准的语义。在CSS中,ID不仅具有太多的特定权重,而且必须是唯一的。 - Imperative
2
@snrlx 我得到了一个空白的PDF和这个错误:renderer.pdf.sHashCode不是一个函数。 - Lisa Solomon
7
如果你想忽略某些元素,你需要用 ID 标记它们。这个美妙的库被那个倒置的要求搞砸了。原帖中想要打印单个 <div>,但可能有数百个 - 所以他必须标记所有不想要的 DOM 元素吗? - Mawg says reinstate Monica
17
如果我没错的话,doc.fromHTML不再受支持。 - Shardul Birje
5
doc.fromHTML() has been deprecated and replaced with doc.html() - ajsaule
显示剩余19条评论

90

这是一个简单的解决方案。对我有效。您可以使用JavaScript打印概念并将其保存为PDF文件。

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script type="text/javascript">
        $("#btnPrint").live("click", function () {
            var divContents = $("#dvContainer").html();
            var printWindow = window.open('', '', 'height=400,width=800');
            printWindow.document.write('<html><head><title>DIV Contents</title>');
            printWindow.document.write('</head><body >');
            printWindow.document.write(divContents);
            printWindow.document.write('</body></html>');
            printWindow.document.close();
            printWindow.print();
        });
    </script>
</head>
<body>
    <form id="form1">
    <div id="dvContainer">
        This content needs to be printed.
    </div>
    <input type="button" value="Print Div Contents" id="btnPrint" />
    </form>
</body>
</html>

44
“我错过了那个部分。如何操作才能将其保存为PDF文件?” - Mawg says reinstate Monica
12
这对我很有用,解决了 CSS 样式的问题。我创建了另一个名为 printPDF.css 的 CSS 文件,并像上面的例子一样使用链接标签添加了它: printWindow.document.write('<html><head><title>DIV Contents</title>'); printWindow.document.write('<link rel="stylesheet" href="../css/printPDF.css" />'); printWindow.document.write('</head><body >'); - Prime_Coder
3
一些评论:1)打印时不需要特定的样式表。在您当前的样式表中,可以进行以下操作:@media print { .pageBreak { page-break-before: always; }.labelPdf { font-weight: bold; font-size: 20px; } .noPrintPdf { display: none; }}然后根据您的需求使用这些类。2)对于我而言,“.live(“click”,...)”无法工作,所以我改用“.on(“click”,...)”。 - Davidson Lima
3
这段代码创建了一个新窗口,该窗口不会看到旧窗口的CSS样式。这就是为什么它会“忽略”CSS的原因,实际上并不是在忽略,只是在新窗口中没有加载CSS。只需在<head>标签中加载CSS,就可以渲染出来。 - Lukas Liesis
2
如果有人想在Angular中实现这个,请将CSS文件放在assets文件夹中。 - rgantla
显示剩余8条评论

33
  • 没有依赖,纯JS
  • 添加CSS或图像时,请勿使用相对URL,而应使用完整的URLhttp://...domain.../path.css等。它会创建单独的HTML文档,并且没有主要内容的上下文。
  • 您还可以将图像嵌入为base64

这些年来一直为我服务:

export default function printDiv({divId, title}) {
  let mywindow = window.open('', 'PRINT', 'height=650,width=900,top=100,left=150');

  mywindow.document.write(`<html><head><title>${title}</title>`);
  mywindow.document.write('</head><body >');
  mywindow.document.write(document.getElementById(divId).innerHTML);
  mywindow.document.write('</body></html>');

  mywindow.document.close(); // necessary for IE >= 10
  mywindow.focus(); // necessary for IE >= 10*/

  mywindow.print();
  mywindow.close();

  return true;
}
当然,这将会打开打印对话框,用户需要知道他/她可以选择“打印为PDF”选项,以获取PDF。可能会预先选择打印机,如果用户确认,则可能实际上打印该文档。为避免这种情况并提供没有任何额外内容的PDF文件,您需要制作PDF文件。可能是在服务器端。您可以有一个仅包含发票的小型HTML页面,并使用无头Chrome将其转换为PDF文件。使用Puppeteer非常容易。不需要安装/配置Chrome,只需安装npm包Puppeteer(由Chrome团队管理)并运行它即可。请记住,这实际上会启动真正的Chrome而没有GUI,因此您需要一些RAM和CPU。大多数服务器在流量足够低的情况下都可以胜任。以下是代码示例,但必须在后端上运行。Nodejs。另外,这是慢调用,也是资源密集型调用。您应该在发票创建后而不是API调用后运行它-为其创建PDF并存储,如果尚未生成PDF,则只需显示消息以在几分钟后再次尝试。
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://your-domain.com/path-to-invoice', {
    waitUntil: 'networkidle2',
  });
  await page.pdf({ path: 'invoice-file-path.pdf', format: 'a4' });

  await browser.close();
})();

了解更多信息请点击这里:https://pptr.dev/


3
问题在于 PDF 文件中不会出现任何 CSS 效果。 - Shadab Faiz
2
@ShadabFaiz 它会生效,但可能与主窗口不同。您仍然可以将自定义 CSS 添加到此 HTML 中。 - Lukas Liesis
2
不过,它不会呈现图像。 - Jan Pi
3
我喜欢这个!稍微调整一下,它看起来很好。还有一件小事,不要删除<body>标签中的额外空格,它需要这个 :P - phrogg
1
@ДаянаДимитрова,如果您的值在HTML中,则将打印来自URL的HTML。 如果您使用PHP,则需要在服务器端呈现。 该代码实际上会打开浏览器,导航到URL并进行打印。 您可以阅读puppeteer API文档,可以填写表单或在页面加载后执行任何JS,因此这可能是您要查找的内容。 但是,对于您的情况,只需使用PHP构建专用终端可能更容易。 对于第一个示例,它将从页面中取出HTML,因此您可以呈现包含HTML内部输入值的隐藏div,并使用该div进行打印。 - Lukas Liesis
显示剩余8条评论

21

如果您需要下载特定页面的PDF文件,只需添加以下按钮:

<h4 onclick="window.print();"> Print </h4>

使用window.print()来打印整个页面而不仅仅是一个div。


5
如果您想创建一个可下载的iframe pdf文件,只需简单添加以下内容,然后使用开发人员控制台: document.querySelector("#myIframe").contentWindow.print() - ioCron
它不会打印多页HTML,只能打印单页PDF。 - The Coder
这在安卓的Chrome上不起作用。 - davejoem

19

如前所述,你应该使用jsPDFhtml2canvas。我还在jsPDF的问题中找到了一个函数,可以自动将你的pdf拆分成多个页面(来源)。

function makePDF() {

    var quotes = document.getElementById('container-fluid');

    html2canvas(quotes, {
        onrendered: function(canvas) {

        //! MAKE YOUR PDF
        var pdf = new jsPDF('p', 'pt', 'letter');

        for (var i = 0; i <= quotes.clientHeight/980; i++) {
            //! This is all just html2canvas stuff
            var srcImg  = canvas;
            var sX      = 0;
            var sY      = 980*i; // start 980 pixels down for every new page
            var sWidth  = 900;
            var sHeight = 980;
            var dX      = 0;
            var dY      = 0;
            var dWidth  = 900;
            var dHeight = 980;

            window.onePageCanvas = document.createElement("canvas");
            onePageCanvas.setAttribute('width', 900);
            onePageCanvas.setAttribute('height', 980);
            var ctx = onePageCanvas.getContext('2d');
            // details on this usage of this function: 
            // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images#Slicing
            ctx.drawImage(srcImg,sX,sY,sWidth,sHeight,dX,dY,dWidth,dHeight);

            // document.body.appendChild(canvas);
            var canvasDataURL = onePageCanvas.toDataURL("image/png", 1.0);

            var width         = onePageCanvas.width;
            var height        = onePageCanvas.clientHeight;

            //! If we're on anything other than the first page,
            // add another page
            if (i > 0) {
                pdf.addPage(612, 791); //8.5" x 11" in pts (in*72)
            }
            //! now we declare that we're working on that page
            pdf.setPage(i+1);
            //! now we add content to that page!
            pdf.addImage(canvasDataURL, 'PNG', 20, 40, (width*.62), (height*.62));

        }
        //! after the for loop is finished running, we save the pdf.
        pdf.save('test.pdf');
    }
  });
}

4
它无法转换图片。 - Probosckie
2
谢谢您的回答,您能给我一些提示如何将它放入A4页面格式吗? - johannesMatevosyan
4
这并不是一个好的矢量PDF,它使用画布创建了很多位图,并将它们叠加成图片。结果有很多缺点——文件大、质量低、无法从PDF中复制和粘贴等。 - Roman Zenka
1
我编写了这个示例来覆盖多个页面上的图像:https://jsfiddle.net/jfr34mgL/ - Ryan Loggerythm

18

我使用 jspdfhtml2canvas 进行 CSS 渲染,并导出特定 div 的内容。以下是我的代码:

$(document).ready(function () {
    let btn=$('#c-oreder-preview');
    btn.text('download');
    btn.on('click',()=> {

        $('#c-invoice').modal('show');
        setTimeout(function () {
            html2canvas(document.querySelector("#c-print")).then(canvas => {
                //$("#previewBeforeDownload").html(canvas);
                var imgData = canvas.toDataURL("image/jpeg",1);
                var pdf = new jsPDF("p", "mm", "a4");
                var pageWidth = pdf.internal.pageSize.getWidth();
                var pageHeight = pdf.internal.pageSize.getHeight();
                var imageWidth = canvas.width;
                var imageHeight = canvas.height;

                var ratio = imageWidth/imageHeight >= pageWidth/pageHeight ? pageWidth/imageWidth : pageHeight/imageHeight;
                //pdf = new jsPDF(this.state.orientation, undefined, format);
                pdf.addImage(imgData, 'JPEG', 0, 0, imageWidth * ratio, imageHeight * ratio);
                pdf.save("invoice.pdf");
                //$("#previewBeforeDownload").hide();
                $('#c-invoice').modal('hide');
            });
        },500);

        });
});

7
这个功能可以运行,但它会将内容转换成图片。 - Samad
3
如何设置分页符,使得如果内容或图片放不下当前页,它们会自动打印在新的一页上? - SHEKHAR SHETE
对于转换为图像的内容,按顺序命名每个div,“<div id ='d1'> <div id ='d2'> <div id ='d3'>”等等。当将它们转换为PDF时,通过数组引用它们....就像这样:html2canvas($(“#div_pdf”+ i)[0])。然后(function(canvas){变量i可以是常见类的结果:var clases = document.querySelectorAll(“.export_pdf”); var len = clases.length; - Pablo Contreras
谢谢你,Ghazaleh,你帮我省了些时间 :) - Mohsen

18
您可以使用autoPrint()函数,并将输出设置为'dataurlnewwindow',如下所示:

function printPDF() {
    var printDoc = new jsPDF();
    printDoc.fromHTML($('#pdf').get(0), 10, 10, {'width': 180});
    printDoc.autoPrint();
    printDoc.output("dataurlnewwindow"); // this opens a new popup,  after this the PDF opens the print window view but there are browser inconsistencies with how this is handled
}

1
我很好奇,除了楼主之外,这个方法有没有对其他人有效过?从代码中看,我似乎理解它只能用于具有ID的元素。不过这可能比较复杂,无论如何我不知道如何让它工作。 - Michael
19
根据我的观察,非常讽刺的是,只有在不包含任何HTML的情况下,才能从HTML中提取信息:只支持纯文本。在我看来,这有点违背了整个事情的初衷。 - Michael
对我来说完美地工作了。您想传递的元素不一定需要具有ID。那只是复制品找到要传递的节点的方式。此外,此解决方案也可以在没有“printDoc.autoPrint()”的情况下工作。如果您想在代码中保留此特定行,则需要包括autoPrint插件。 - snrlx
2
.fromHTML() has been deprecated and replaced with .html() - ajsaule

12

一种方法是使用window.print()函数,它不需要任何库

优点

1.不需要外部库。

2.我们还可以仅打印所选的部分。

3.没有css冲突和js问题。

4.核心html/js功能

---只需添加以下代码

CSS

@media print {
        body * {
            visibility: hidden; // part to hide at the time of print
            -webkit-print-color-adjust: exact !important; // not necessary use         
               if colors not visible
        }

        #printBtn {
            visibility: hidden !important; // To hide 
        }

        #page-wrapper * {
            visibility: visible; // Print only required part
            text-align: left;
            -webkit-print-color-adjust: exact !important;
        }
    }

JS代码 - 在按钮点击时调用下面的函数

$scope.printWindow = function () {
  window.print()
}

注:在每个CSS对象中使用!important

示例 -

.legend  {
  background: #9DD2E2 !important;
}

2
浏览器的打印功能存在问题。用户通常选择默认选项进行打印预览(边距、页面大小等)。因此,如果没有对用户进行培训,就很难生成所需样式的PDF文件,这是非常困难且几乎不可能的事情。 - Rahmat Ali

9

2022年答案:

从HTML元素生成PDF并提示保存文件:

import { jsPDF } from "jsPDF"

function generatePDF() {
  const doc = new jsPDF({ unit: 'pt' }) // create jsPDF object
  const pdfElement = document.getElementById('pdf') // HTML element to be converted to PDF

  doc.html(pdfElement, {
    callback: (pdf) => {
      pdf.save('MyPdfFile.pdf')
    },
    margin: 32, // optional: page margin
    // optional: other HTMLOptions
  })
}

<button onclick="generatePDF()">Save PDF</button>

预览PDF文件而不打印:

doc.html(pdfElement, {
  callback: (pdf) => {
    const myPdfData = pdf.output('datauristring')
  }
})

<embed type="application/pdf" src={myPdfData} />

以下是更多HTMLOptions的相关信息:
https://github.com/parallax/jsPDF/blob/master/types/index.d.ts


它在哪里?这个无法加载 https://www.npmjs.com/package/jspdf - chad steele
jsPDF 无法处理复杂的布局和样式,仅适用于简单页面。 - Zortext
html方法不存在。 - user3953989

6
使用 pdfMake.js这个Gist
(我发现这个 Gist 在这里,还有一个链接到 html-to-pdfmake 的包,但目前我没有使用。)
安装完 npm install pdfmake 并将 Gist 保存在 htmlToPdf.js 中后,我这样使用它:
const pdfMakeX = require('pdfmake/build/pdfmake.js');
const pdfFontsX = require('pdfmake-unicode/dist/pdfmake-unicode.js');
pdfMakeX.vfs = pdfFontsX.pdfMake.vfs;
import * as pdfMake from 'pdfmake/build/pdfmake';
import htmlToPdf from './htmlToPdf.js';

var docDef = htmlToPdf(`<b>Sample</b>`);
pdfMake.createPdf({content:docDef}).download('sample.pdf');

备注:

  • 我的使用场景是从 Markdown 文档 (使用 markdown-it) 创建关联的 HTML,然后生成 PDF 并上传其二进制内容 (可以通过 pdfMakegetBuffer() 函数获取),所有这些都是在浏览器中完成的。与我尝试过的其他解决方案相比,生成的 PDF 对于此类 HTML 来说效果更好。
  • 我对接受的答案中建议使用的 jsPDF.fromHTML() 得到的结果不满意,因为该解决方案会被 HTML 中的特殊字符所迷惑,导致生成的 PDF 完全混乱。
  • 像基于 Canvas 的解决方案 (例如已停用的 jsPDF.from_html() 函数,不要与接受的答案中的函数混淆) 不适用于我,因为我希望生成的 PDF 中的文本可粘贴,而基于 Canvas 的解决方案会生成基于位图的 PDF。
  • 直接从 Markdown 转换为 PDF 的转换器 (例如 md-to-pdf) 仅限于服务器端,对我无法使用。
  • 使用浏览器的打印功能对我来说行不通,因为我不想显示生成的 PDF,而是上传其二进制内容。

如果我正确地阅读了代码,那么它不支持CSS边框样式(例如在表格上),对吗? - ninjagecko
离题了,我使用pdfmake创建PDF,而不是从HTML中获取内容。我的问题是:如何提供我们自己的特定文件名,而不是在使用其方法:pdfMake.createPdf(docDefinition).open()时生成随机文件名? - Lex Soft
现在回到主题,你提到的要点并不存在。你说你最终没有使用html-to-pdfmake,那么它有什么问题呢?我在github上看到它最近还在维护。 - Lex Soft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接