从React组件生成PDF文件

106

我一直在开发一个投票应用程序。用户能够创建自己的投票并获取有关所提问的数据。我想添加功能,使用户可以将结果以PDF格式下载。

例如,我有两个组件负责获取问题和数据。

<QuestionBox />
<ViewCharts />

我想把这两个组件输出到一个PDF文件中,用户可以下载这个PDF文件。我找到了几个可以在组件内部呈现PDF的软件包。但是,我没有找到可以从虚拟DOM组成的输入流生成PDF的软件包。如果我想从头开始实现这个功能,应该采用什么方法?


3
根据这个问题和答案,我写了两篇关于这个主题的简短教程:React组件转PDFReact组件转图片 - Robin Wieruch
@RobinWieruch 很棒的文章,但如果组件足够大而无法放在一页上怎么办?我刚刚按照你的教程操作,但如果有适量的内容,它并不会全部显示。你知道如何做一个“分页”并开始新的一页吗?谢谢! - Peter Malik
@PeterMalik 也许你可以尝试横向模式。另一种选择是在生成图像之前触发CSS媒体查询,以便媒体查询将内容呈现为更适合PDF的形状。 - Robin Wieruch
13个回答

159

将React渲染成PDF通常很麻烦,但可以使用canvas解决。

思路是将: HTML -> Canvas -> PNG(或JPEG) -> PDF

为了实现上述目标,您需要:

  1. html2canvas
  2. jsPDF

import React, {Component, PropTypes} from 'react';

// download html2canvas and jsPDF and save the files in app/ext, or somewhere else
// the built versions are directly consumable
// import {html2canvas, jsPDF} from 'app/ext';


export default class Export extends Component {
  constructor(props) {
    super(props);
  }

  printDocument() {
    const input = document.getElementById('divToPrint');
    html2canvas(input)
      .then((canvas) => {
        const imgData = canvas.toDataURL('image/png');
        const pdf = new jsPDF();
        pdf.addImage(imgData, 'JPEG', 0, 0);
        // pdf.output('dataurlnewwindow');
        pdf.save("download.pdf");
      })
    ;
  }

  render() {
    return (<div>
      <div className="mb5">
        <button onClick={this.printDocument}>Print</button>
      </div>
      <div id="divToPrint" className="mt4" {...css({
        backgroundColor: '#f5f5f5',
        width: '210mm',
        minHeight: '297mm',
        marginLeft: 'auto',
        marginRight: 'auto'
      })}>
        <div>Note: Here the dimensions of div are same as A4</div> 
        <div>You Can add any component here</div>
      </div>
    </div>);
  }
}

由于未导入所需文件,此代码片段在此处将无法运行。

另一种方法在此答案中被使用,其中省略了中间步骤,您可以直接从HTML转换为PDF。在jsPDF文档中也有这样的选项,但是从个人观察来看,当dom先转换为png时可以获得更好的准确性。

更新0:2018年9月14日

使用此方法创建的pdf上的文本将不可选择。如果这是要求,您可能会发现这篇文章会很有帮助。


2
我已经成功地通过您的答案从html页面创建了一个PDF文件。但是如果我们想要创建一个有多个页面的PDF怎么办?每一页都应该有A4尺寸和适当的边距,新页面需要应用相同的边距。请帮帮我,这很紧急。提前感谢您。我在这里提出了一个问题 question - Keyur Android
2
@ShivekKhurana 我的 div 根据屏幕大小而变化,但我希望 PDF 文件始终保持相同大小,不受窗口大小影响。有什么办法可以做到这一点吗?此外,如何导入 {...css}? - André Almeida
2
通过这种方法,可以复制创建的 PDF 上的文本吗? - Goutham
这种方法如果在div中包含一张图片是行不通的。有没有解决方案的文本以及图片? - Jaison James
@ShivekKhurana 如何在React TypeScript项目中使用上述代码? - Kousalya
显示剩余7条评论

13

React-PDF 是一个很好的资源。

将你的标记和CSS转换成React-PDF的格式有点耗时,但易于理解。导出PDF也相当简单。

要允许用户下载由React-PDF生成的PDF,请使用他们的即时渲染,提供可自定义的下载链接。当用户点击下载链接时,网站会呈现并下载PDF文件。

这是他们的REPL,可以让您熟悉所需的标记和样式。他们也有一个PDF下载链接,但这里没有显示该代码。


12

只需几个步骤,我们就可以从我们的HTML页面下载或生成PDF,或者我们可以从HTML页面生成特定div的PDF。

步骤:HTML -> 图像(PNG或JPEG) -> PDF

请按照以下步骤操作:

第1步:

npm install --save html-to-image
npm install jspdf --save

第二步:

/* ES6 */
import * as htmlToImage from 'html-to-image';
import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'html-to-image';
 
/* ES5 */
var htmlToImage = require('html-to-image');

-------------------------
import { jsPDF } from "jspdf";

步骤三:

   ******  With out PDF properties given below  ******

 htmlToImage.toPng(document.getElementById('myPage'), { quality: 0.95 })
        .then(function (dataUrl) {
          var link = document.createElement('a');
          link.download = 'my-image-name.jpeg';
          const pdf = new jsPDF();          
          pdf.addImage(dataUrl, 'PNG', 0, 0);
          pdf.save("download.pdf"); 
        });


    ******  With PDF properties given below ******

    htmlToImage.toPng(document.getElementById('myPage'), { quality: 0.95 })
        .then(function (dataUrl) {
          var link = document.createElement('a');
          link.download = 'my-image-name.jpeg';
          const pdf = new jsPDF();
          const imgProps= pdf.getImageProperties(dataUrl);
          const pdfWidth = pdf.internal.pageSize.getWidth();
          const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
          pdf.addImage(dataUrl, 'PNG', 0, 0,pdfWidth, pdfHeight);
          pdf.save("download.pdf"); 
        });

我认为这很有帮助。请尝试。


链接和link.download是必要的吗? - Peter Collingridge
not needed though - Shyam Mittal
这对我来说非常容易。 - Devashish

11

你可以使用jsPDF结合画布(canvas)

import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';

 _exportPdf = () => {

     html2canvas(document.querySelector("#capture")).then(canvas => {
        document.body.appendChild(canvas);  // if you want see your screenshot in body.
        const imgData = canvas.toDataURL('image/png');
        const pdf = new jsPDF();
        pdf.addImage(imgData, 'PNG', 0, 0);
        pdf.save("download.pdf"); 
    });

 }

你的id为capture的div是:

example

<div id="capture">
  <p>Hello in my life</p>
  <span>How can hellp you</span>
</div>

5
你为什么要添加答案的副本? - Micah B.
图片质量不好,生成的PDF文件的A4页面部分被裁剪了。这个问题在react-to-pdf和jdpdf库中都存在。 - Greko2015 GuFn
1
这种解决方案的唯一问题是它将捕获整个div的图像,但会生成部分pdf - 而不是完整的pdf。特别是当div太宽时。简单的解决方法是替换两行代码。一行是jspdf构造函数行const pdf = new jsPDF('p', 'pt', 'a4', false);,第二行是将图像粘贴到pdf行pdf.addImage(imgData, 'PNG', 0, 0, 600, 0, undefined, false);。如果您想调整pdf中捕获图像的宽度或高度,则可以玩弄addImage函数的第5个参数。疯狂一下吧 :P - imran haider

6
除了使用 html2canvasjspdf 库之外,我发现还有一个简单易用的库叫做 react-to-print,可以将 React 组件下载为 PDF 文件,这里是该库的链接:https://www.npmjs.com/package/react-to-print。 它的文档、示例和演示都非常棒。 它甚至为基于类和基于函数的组件用户提供了文档。
下面是一个使用基于函数的组件的示例:
import React, { useRef } from 'react';
import { useReactToPrint } from 'react-to-print';

import { ComponentToPrint } from './ComponentToPrint';

const Example = () => {
  const componentRef = useRef();
  const handlePrint = useReactToPrint({
    content: () => componentRef.current,
  });

  return (
    <div>
      <ComponentToPrint ref={componentRef} />
      <button onClick={handlePrint}>Print this out!</button>
    </div>
  );
};

这不是将组件下载为PDF。它只是显示了一个PDF预览。 - undefined

6
my opinion : 

import { useRef } from "react";
import { jsPDF } from "jspdf";
import html2canvas from "html2canvas";

import "./styles.css";

const App = () => {
  const inputRef = useRef(null);
  const printDocument = () => {
    html2canvas(inputRef.current).then((canvas) => {
      const imgData = canvas.toDataURL("image/png");
      const pdf = new jsPDF();
      pdf.addImage(imgData, "JPEG", 0, 0);
      pdf.save("download.pdf");
    });
  };
  return (
    <>
      <div className="App">
        <div className="mb5">
          <button onClick={printDocument}>Print</button>
        </div>
        <div id="divToPrint" ref={inputRef}>
          <div>Note: Here the dimensions of div are same as A4</div>
          <div>You Can add any component here</div>
        </div>
      </div>
    </>
  );
};
export default App;

link demo:

https://codesandbox.io/s/frosty-sea-44b4q?file=/src/App.js:0-907

5
你可以使用ReactDOMServer将你的组件渲染为HTML,然后在jsPDF上使用。
首先进行导入:
import React from "react";
import ReactDOMServer from "react-dom/server";
import jsPDF from 'jspdf';

然后:

var doc = new jsPDF();
doc.fromHTML(ReactDOMServer.renderToStaticMarkup(this.render()));
doc.save("myDocument.pdf");

建议使用:

renderToStaticMarkup

而不是:

renderToString

因为前者包含react所依赖的HTML代码。


2
看起来很有趣,但是renderToStaticMarkup不返回任何样式,所以据我所知,除非有一种注入CSS类的方法,否则jsPDF只会呈现原始HTML的默认布局。 - Simon Hutchison

3
这可能不是最优的做法,但我发现解决多页面问题的最简单方法是确保在调用jsPDFObj.save方法之前完成所有渲染。
至于渲染隐藏文章,可以通过类似于css图像文本替换的修复方法来解决,将要渲染的元素绝对定位为页面左侧-9999px,这不影响布局,并允许elem可见于html2pdf,特别是在使用选项卡、手风琴和其他依赖于{display: none}的UI组件时。
该方法在promise中包装先决条件,并在finally()方法中调用pdf.save()。我不能确定这是否是万无一失的,或者是一个反模式,但它似乎适用于我所遇到的大多数情况。

// Get List of paged elements.
let elems = document.querySelectorAll('.elemClass');
let pdf = new jsPDF("portrait", "mm", "a4");

// Fix Graphics Output by scaling PDF and html2canvas output to 2
pdf.scaleFactor = 2;

// Create a new promise with the loop body
let addPages = new Promise((resolve,reject)=>{
  elems.forEach((elem, idx) => {
    // Scaling fix set scale to 2
    html2canvas(elem, {scale: "2"})
      .then(canvas =>{
        if(idx < elems.length - 1){
          pdf.addImage(canvas.toDataURL("image/png"), 0, 0, 210, 297);
          pdf.addPage();
        } else {
          pdf.addImage(canvas.toDataURL("image/png"), 0, 0, 210, 297);
          console.log("Reached last page, completing");
        }
  })
  
  setTimeout(resolve, 100, "Timeout adding page #" + idx);
})

addPages.finally(()=>{
   console.log("Saving PDF");
   pdf.save();
});


2
你可以使用我的库react-pdf-printer
该库将帮助你控制页面尺寸,指定网站在哪里分成多个页面,或设置具有分页的自定义页眉和页脚等方面进行自定义。
我希望它能帮助到你或其他人。

2
npm install jspdf --save

//React中的代码

import jsPDF from 'jspdf';


var doc = new jsPDF()


 doc.fromHTML("<div>JOmin</div>", 1, 1)


onclick //

 doc.save("name.pdf")

@Felix,HTML字符串允许使用<style>标签,因此您必须编写内联CSS。 - Matias Faure

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