700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > React jsPdf+html2canvas 前端生成pdf(分页截断 + 图片质量)

React jsPdf+html2canvas 前端生成pdf(分页截断 + 图片质量)

时间:2020-04-24 16:03:23

相关推荐

React jsPdf+html2canvas 前端生成pdf(分页截断 + 图片质量)

前言

记录一下最近实现的前端生成pdf的功能,以及遇到的坑和解决方式

由于笔者1年多没碰过react了,之前也比较少使用hooks,实现这个功能也是想复习一下之前的知识,如果有什么写得不好的请指出交流。

正文

安装lib

npm install -S jspdf html2canvas

jspdf+html2canvas, 基本的实现逻辑使用html2canvas将html放入canvas,然后将canvas转成base64的图片,再用jspdf.addImage()将图片放入pdf。

我们的目标实现是将这个功能封装成一个可复用的component,后续使用只需要传入header,content,footer, 就可以生成一份带有页眉页尾的pdf。

index.js

import React, { Component, useState, useMemo, useRef, useEffect } from 'react';import PdfComponent from '@/components/PdfComponent';import img from '@/asset/img/三姐妹泉中的西印度海牛,佛罗里达州克里斯特尔里弗国家野生动物保护区.jpg'import logo from '@/asset/logo/dog.jpg'import './index.css';import moment from 'moment';import mockData from './mockData';function HeaderComponent(props) {return <div style={{ padding: '10px 0' }}><img src={logo} /><span>Header</span><hr /></div>}function Pdf() {const pdfRef = useRef(null);const downloadPdf = () => {pdfRef.current.downloadPdf();}const Content = [<img id='img' key='img' style={{ maxHeight: '200px', width: 'auto' }} src={img} />,...mockData.map((data, ind) => {return (<div key={data.id}><h3>{data.title}</h3>{(ind % 8 === 0) && <img key={'img' + ind} style={{ maxHeight: '100px', width: 'auto' }} src={img} />}<div style={{color: 'grey',}}>{data.content.map((content, ind) => (<p key={ind}>{content}</p>))}</div></div>)})]return (<><h3>Pdf</h3>{/* 按钮 */}<button onClick={downloadPdf}>Download</button><PdfComponentref={pdfRef} // 组件refplr={23} // 左右边距keepImgQuality={true} // 是否保持图片质量scale={4} // 用于让html放大n倍后再转为canvas 用于修改生成pdf的清晰度,越清晰pdf大小会越大decoratePdf={(pdf, pageTotal, headerSize, footerSize) => { // 装饰pdf,导出pdf对象,用于生成页数之类的修饰信息pdf.setFontSize(9); // 设置字体大小const lineHeight = pdf.getLineHeight(); // 获取行高new Array(pageTotal).fill(0).forEach((item, i) => { // 根据总页数循环添加页码// set page & datepdf.setPage(i + 1);const pageText = (i + 1) + " / " + pageTotal;const timeText = moment().format('YYYY-MM-DD HH:mm');const pageTextX = pdf.internal.pageSize.getWidth() - pdf.getTextWidth(pageText) - 23;const timeTextX = pdf.internal.pageSize.getWidth() - pdf.getTextWidth(timeText) - 23;pdf.text(pageText, pageTextX, (headerSize.h - lineHeight * 2 - 2) / 2);pdf.text(timeText, timeTextX, (headerSize.h - lineHeight * 2 - 2) / 2 + lineHeight + 2);const footerY = pdf.internal.pageSize.getHeight() - footerSize.h + (footerSize.h - lineHeight * 2 - 2) / 2 + lineHeight;pdf.text(pageText, pageTextX, footerY);pdf.text(timeText, timeTextX, footerY + lineHeight + 2);})}}header={HeaderComponent} // 页眉content={Content} // 内容footer={<><hr /><div style={{ padding: '10px 0' }}>{new Array(20).fill(0).map((img, ind) => (<img key={'img' + ind} src={logo} style={{ width: '20px', height: 'auto' }} />))}</div></>}/></>);}export default Pdf;

pdfComponent.less

#pdf {font-size: 8pt;color: #333;background-color: #fff;// 定义文本为垂直排布,使用左对齐,内容从上到下垂直流动,这样在子元素中使用flex布局的wrap,将会在纵向换行,如此便实现了横铺的分页writing-mode: vertical-lr;// 此处在测试完毕之后需要将dom使用绝对定位固定// 因为html2canvas的截图范围似乎和滚动条相关// box-sizing: border-box;// position: fixed;// top: 0pt;// left: 0pt;// left: -9999pt;// z-index: -9999;#pdf-header,#pdf-footer {// 在需要的地方将对其脚本修改回来writing-mode: horizontal-tb;// box-sizing: border-box;}#pdf-content {background-color: #fff;// box-sizing: border-box;display: inline-flex;flex-flow: row wrap;* {// box-sizing: border-box;writing-mode: horizontal-tb;}}}

这个方法部分参考了这篇文章的思路,但是实现方式有部分不同

如果不理解可以使用这个测试html尝试一下

<!DOCTYPE html><html><head><title></title><style type="text/css">.home-category-detail {width: auto;background: #fff;writing-mode: vertical-lr;}.container {height: 50px;display: inline-flex;flex-flow: row wrap;align-items: flex-start;align-content: flex-start;}.inner {width: 20px;height: 20px;border: 1px solid #000;display: inline-block;writing-mode: horizontal-tb;margin: 0px 5px;}</style></head><body><div class="home-category-detail"><div class="container"><div class="inner">1</div><div class="inner">2</div><div class="inner">3</div><div class="inner">4</div><div class="inner">5</div><div class="inner">6</div><div class="inner">7</div><div class="inner"></div><div class="inner"></div><div class="inner"></div><div class="inner"></div><div class="inner"></div><div class="inner"></div><div class="inner"></div><div class="inner"></div></div></div></body></html>

PdfComponent

import React, { Component, useState, useMemo, useRef, useEffect, useImperativeHandle, forwardRef } from 'react';import { jsPDF } from 'jspdf';import html2canvas from 'html2canvas';import moment from 'moment';import {isFunction} from 'lodash';import './index.less';// 加载图片,用于获取图片转成base64再重新放入pdf,保证图片质量const loadImage = async (src) => {return new Promise((resolve, reject) => {let canvas = document.createElement('canvas');let ctx = canvas.getContext('2d');let img = new Image;// 此处如果不添加crossOrigin,canvas会觉得画布收到污染,是脏的,所以需要添加这个属性// 添加之后说明是以跨域发起的图片请求,所以后端服务器需要允许跨域 Access-Control-Allow-Origin: *img.crossOrigin = 'Anonymous';img.src = src;img.onload = function () {canvas.height = img.height;canvas.width = img.width;ctx.drawImage(img, 0, 0);let dataURL = canvas.toDataURL('image/png');canvas = null;resolve(dataURL);};img.onerror = function (e) {console.log('e', e)resolve(url);}});}function PdfComponent(props, ref) {// 保存A4纸的宽高,单位ptconst a4Size = {w: 595.28,h: 841.89};const {plr,header,content,footer,decoratePdf,scale = 2, // 放大倍数 清晰度相关keepImgQuality = false // 是否保证图片质量} = props;const pdfHeaderRef = useRef(null);const pdfContentRef = useRef(null);const pdfFooterRef = useRef(null);// 存储pdf content的宽高const [pdfContentSize, setPdfContentSize] = useState({w: a4Size.w - 2 * plr,h: a4Size.h})// 保存domconst [pdfHeader, setPdfHeader] = useState(header);const [pdfContent, setPdfContent] = useState(content);const [pdfFooter, setPdfFooter] = useState(footer);// 由于pdf中的单位是英寸,所以在这里需要计算pt和px的比例const getRate = () => {const dom = document.createElement("div");dom.style.width = a4Size.w + 'pt';document.body.appendChild(dom);const domWidth = Math.ceil(dom.clientWidth);document.body.removeChild(dom);return a4Size.w / domWidth;}const rate = getRate();// px转为pt的方法const px2pt = (px) => {return Math.ceil(px * rate);}// 封装将dom转为img的方法const generatePdfImg = async (dom, options = {}) => {const {scale = 4,quality = 1} = options;const canvas = await html2canvas(dom,{ scale, y: 0, scrollY: 0 });let img = canvas.toDataURL('image/jpeg', quality);return {img,// 在这离存储dom生成后的img的宽高 pximgSize: {w: dom.clientWidth,h: dom.clientHeight}};}// 将pdf中的所有图片信息保存起来// 包括图片base64,宽高,距离生成后dom的定位const getPdfImgObjArr = async() => {const pdfContentDom = pdfContentRef.current;const imgCollection = pdfContentDom.getElementsByTagName('img');return await Promise.all(Array.from(imgCollection).map(async (img) => {const imgObj = {img: await loadImage(img.src),h: img.clientHeight,w: img.clientWidth,x: img.offsetLeft - pdfContentDom.offsetLeft,y: img.offsetTop - pdfContentDom.offsetTop}img.style.opacity = 0;img.style.width = img.clientWidth + 'px';img.style.height = img.clientHeight + 'px';// img.src = '';return imgObj;}))}// 为页首/内容/页尾添加样式useEffect(() => {const pdfHeaderDom = pdfHeaderRef.current;const pdfContentDom = pdfContentRef.current;const pdfFooterDom = pdfFooterRef.current;// ehance component// 增强组件,为内容数组传进来的每一行添加宽度和左右paddingconst enhanceComp = (c, ind) => (<div style={{width: pdfContentSize.w + 'pt',paddingLeft: plr + 'pt',paddingRight: plr + 'pt'}} key={ind}>{c}</div>);setPdfHeader(enhanceComp(pdfHeader));setPdfContent(pdfContent.map(enhanceComp));setPdfFooter(enhanceComp(pdfFooter));// 设置内容宽度/高度// 此处计算的是pdf中content允许存放的真实大小setPdfContentSize({...pdfContentSize,h: px2pt(pdfContentDom.clientHeight - pdfHeaderDom.clientHeight - pdfFooterDom.clientHeight)});}, []);// 暴露导出事件useImperativeHandle(ref, () => ({// 导出事件downloadPdf: async () => {const contentEle = pdfContentRef.current;const contentWidthPt = px2pt(contentEle.clientWidth);// 计算总页数const pageTotal = Math.ceil(contentWidthPt / a4Size.w);const pdf = new jsPDF('p', 'pt', 'a4');// 先创建出需要的页数new Array(pageTotal - 1).fill(0).forEach(() => pdf.addPage());// 生成三张图片, 页眉 内容 页尾const { img: headerImg, imgSize: headerImgSize } = await generatePdfImg(pdfHeaderRef.current, {scale});const { img: contentImg, imgSize: contentImgSize } = await generatePdfImg(contentEle, {scale});const { img: footerImg, imgSize: footerImgSize } = await generatePdfImg(pdfFooterRef.current, {scale});for (let i = 1; i <= pageTotal; i++) {pdf.setPage(i);// 根据定位和大小将图片放入pdf// (距离左边的距离,距离上边的距离,宽度,高度) ptpdf.addImage(headerImg, 'JPEG', 0, 0, a4Size.w, px2pt(headerImgSize.h));pdf.addImage(contentImg, 'JPEG', -(i - 1) * a4Size.w, px2pt(headerImgSize.h), a4Size.w * pageTotal, px2pt(contentImgSize.h));pdf.addImage(footerImg, 'JPEG', 0, px2pt(headerImgSize.h + contentImgSize.h), a4Size.w, px2pt(footerImgSize.h));}// 如果需要保证图片质量(即放大后依旧清晰)keepImgQuality && (await getPdfImgObjArr()).forEach(item => {const pageNumber = Math.ceil(item.x / (contentImgSize.w / pageTotal));pdf.setPage(pageNumber);pdf.addImage(item.img, 'JPEG', px2pt(item.x - (contentImgSize.w / pageTotal * (pageNumber - 1))), px2pt(item.y + headerImgSize.h),px2pt(item.w), px2pt(item.h));});// 是否需要装饰,生成页码isFunction(decoratePdf) && decoratePdf(pdf, pageTotal,{w: px2pt(headerImgSize.w), h: px2pt(headerImgSize.h)},{w: px2pt(footerImgSize.w), h: px2pt(footerImgSize.h)})pdf.save(`pdf`);return;}}), [pdfContentRef, pdfContentSize]);return (<div><div id="pdf" ><div ref={pdfHeaderRef} id="pdf-header">{pdfHeader}</div><div ref={pdfContentRef} id="pdf-content"style={{height: pdfContentSize.h + 'pt',}}>{pdfContent}</div ><div ref={pdfFooterRef} id="pdf-footer">{pdfFooter}</div></div ></div>);}export default forwardRef(PdfComponent);

效果图

主要的缺点是分页只能根据传入content的数组的每个item来分页,如果某个item的内容超过了一整页,那将会被截断。

不过使用这个组件的话,一些比较基本的定制还是能ok的,例如导出表格,文档之类的。

谢谢。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。