html转PDF文件最完美的方案(wkhtmltopdf)

目录

需求

一、方案调研

二、wkhtmltopdf使用

如何使用

文档简要说明

三、后端服务

四、前端服务

往期回顾


需求

最近在做报表类的统计项目,其中有很多指标需要汇总,网页内容有大量的echart图表,做成一个网页去浏览,同时需要转成PDF格式下载浏览,更重要的是pdf格式再打开后,需要自定义页眉、页脚,页码,支持文本的选中、复制、粘贴,同时左侧也要有正常的页签导航,点击哪里到哪里。

一、方案调研

经过调研主要有以下几种方式生成pdf,但是每个方案都有缺陷,跟我们的需求相差。

方案优点缺点
window.print()1、兼容性最好
2、可以将任意内容导出成 pdf 文档, 甚至是非改页面上的内容

1、调用方法时部分条件下导出pdf需要用户手动选择

2、生成的pdf不支持生成页签导航

3、页眉页脚不适合自定义

 jspdf + html2canvas1、在jspdf上将生成效果不佳的部分可以转成图片,适用于对样式有要求的场景
2、将乱码部分转为了图片,解决了中文乱码问题
3、没有预览点击即可保存

1、如果内容包含echart图表或者其它图表,该内容需要转图片
2、生成的pdf实际为图片,不支持复制
3、不同浏览器生成可能会有略微差异(页面周边留白部分差异)
 4、由于整体效果为图片,导致pdf文件较大(两页2.5MB左右)

5、pdf分页不好处理

6、不支持生成页签导航

wkhtmltopdf

1、支持自定义页眉页脚页码

2、支持文本选中粘贴复制

3、支持将html的h标签自动生成pdf

1、需要结合后端去实现生成接口返回给前端下载

2、wkhtmltopdf 使用 WebKit 渲染引擎,这意味着它在某些情况下可能无法完全支持所有现代 CSS 和 JavaScript 特性,特别是那些依赖于最新浏览器特性的功能

3、wkhtmltopdf 对 JavaScript 的支持有限。虽然它可以在某些情况下执行简单的 JavaScript,但复杂的交互式内容可能无法正确渲染。

前两种是纯前端去实现的方案,一是用浏览器打印功能实现,这种方案简单粗暴,但是需要手动触发,不支持自定义页眉页脚页码,浏览器也不支持生成页签导航。第二种把整个页面生成图片,完整还原了样式但是,跟我们的要求差太远。第三种是wkhtmltopdf,底层是C++去实现的,能够高效地将 HTML 内容转换为高质量的 PDF 文件。下面主要介绍下wkhtmltopdf使用。

二、wkhtmltopdf使用

官网入口:wkhtmltopdf

如何使用

  1. 下载预编译的二进制文件或从源代码构建

下载链接:wkhtmltopdf

以下是适配所有操作系统的包,我们根据自己的系统不同的下载包

以centeros7为例

1.首先我们下载我们需要的包

 我的是x86_64的,下载完成后将包传到服务器

 运行命令安装

rpm -Uvh wkhtmltox-0.12.6-1.centos7.x86_64.rpm

 报错!!!

原因是缺少依赖,我们来安装下依赖

yum install fontconfig libX11 libXext libXrender libjpeg libpng  xorg-x11-fonts-Type1

yum install -y xorg-x11-fonts-75dpi

 再次运行安装命令

查看版本

wkhtmltopdf --version

 

大功告成!  YYDS! 

安装完成后我们来使用它

  1. 创建要转换为PDF或者图像的HTML文档
  2. 通过命令运行工具生成PDF

比如我要将Google网页保存为pdf,则可以直接运行命令

wkhtmltopdf http://google.com google.pdf

文档简要说明

官方文档说明:https://wkhtmltopdf.org/usage/wkhtmltopdf.txt

强烈建议查看官方文档,以下(基于0.12.6的版本)

1. 基本命令

wkhtmltopdf [选项] <输入文件或URL> <输出PDF文件>

示例:

wkhtmltopdf input.html output.pdf

2.大纲(必要实现)

大纲就是PDF阅读器中,用于显示导航跳转的部分,不属于PDF文档中的一部分,主要是方便阅读器浏览导航使用。

Wkhtmltopdf 用 patched qt 支持PDF大纲(也称为书签),可以通过设置--outline (默认选项)选项实现。

大纲是根据 <h?>(h1–h6) 标签生成的,有关如何实现的详细说明,请参见目录部分。

如果 <h?> 标签在HTML文档中嵌套的层级非常深,那么大纲树的层级也会变得非常深。可以通过--outline-depth选项来设置大纲的层级深度。

详细使用参考这篇文章哈哈哈

wkhtmltopdf 0.12.6 中文文档(精心整理)-CSDN博客

原理是:wkhtmltopdf将整个带css的html文档转为了pdf,因此想要 将我们前端画的好看的页面生成pdf,需要将html文档传给wkhtmltopdf。

三、后端服务

 我们需要写一个后端服务,通过接口将前端绘制的漂亮页面整个以api的方式传给后端,后端将文档内容整理后,调用wkhtmltopdf的命令来生成pdf,然后返回文件流给前端提供下载。

npm为我们提供了调用wkhtmltopdf服务的插件

wkhtmltopdf - npm

以下是简单用法,以官方最新为准

var wkhtmltopdf = require('wkhtmltopdf');// URL
wkhtmltopdf('http://google.com/', { pageSize: 'letter' }).pipe(fs.createWriteStream('out.pdf'));// HTML
wkhtmltopdf('<h1>Test</h1><p>Hello world</p>').pipe(res);// Stream input and output
var stream = wkhtmltopdf(fs.createReadStream('file.html'));// output to a file directly
wkhtmltopdf('http://apple.com/', { output: 'out.pdf' });// Optional callback
wkhtmltopdf('http://google.com/', { pageSize: 'letter' }, function (err, stream) {// do whatever with the stream
});// Repeatable options
wkhtmltopdf('http://google.com/', {allow : ['path1', 'path2'],customHeader : [['name1', 'value1'],['name2', 'value2']]
});// Ignore warning strings
wkhtmltopdf('http://apple.com/', { output: 'out.pdf',ignore: ['QFont::setPixelSize: Pixel size <= 0 (0)']
});
// RegExp also acceptable
wkhtmltopdf('http://apple.com/', { output: 'out.pdf',ignore: [/QFont::setPixelSize/]
});

以下是我写的一个简单的node server.js调用案列

const express = require('express');
const path = require('path');
const app = express();
const port = 3002;// 引入 cors 中间件
const cors = require('cors');// 使用 cors 中间件
app.use(cors());const fs = require('fs');// 解析 JSON 请求体,设置最大限制为 50MB
app.use(express.json({ limit: '50mb' }));// 解析 application/x-www-form-urlencoded 请求体,设置最大限制为 50MB
app.use(express.urlencoded({ extended: true, limit: '50mb' }));// PDF生成高并发处理
function getPdfHeavyTask(html) {const wkhtmltopdf = require('wkhtmltopdf');const options = {output: `./pdfs/demo.pdf`,pageSize: 'letter',orientation: 'portrait',marginTop: '1.8cm',marginBottom: '1.2cm',marginLeft: '1cm',marginRight: '1cm',encoding: 'UTF-8',dpi: 300,zoom: 1,title: 'pdf生成demo',enableSmartShrinking: true,javascriptDelay: 1000,noStopSlowScripts: true,headerHtml: './template/header.html', // 设置页眉模板footerHtml: './template/footer.html' // 设置页脚模板};return new Promise((resolve) => {wkhtmltopdf(html, options, (err, stream) => {if (err) {resolve({ status: 500, data: err });return;}resolve({ status: 200, data: stream });});});
}app.post('/generate-pdf', async (req, res) => {const { content, css } = req.body;let html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>pdf生成demo</title><style>body { font-family: "Microsoft YaHei", "SimSun", sans-serif; }${css}</style></head><body>${content}</body></html>`;// 高并发生成异步任务处理const { status, data } = await getPdfHeavyTask(html);// PDF生成失败if (status === 500) {res.status(500).send(data);return;}// PDF生成成功读取const filePath = path.resolve(__dirname, './pdfs/demo.pdf');const fileStream = fs.createReadStream(filePath);const stat = fs.statSync(filePath);res.setHeader('Content-Length', stat.size);res.setHeader('Content-Type', 'application/pdf');res.setHeader('Content-Disposition', 'attachment; filename=demo.pdf');fileStream.pipe(res);
});app.listen(port, () => {console.log(`Server running at http://localhost:${port}`);
});

页眉页脚代码根据自己的需求添加即可

案例:header.html 自定义页码

 <!DOCTYPE html><html><head><script>function subst() {var vars = {};var query_strings_from_url = document.location.search.substring(1).split('&');for (var query_string in query_strings_from_url) {if (query_strings_from_url.hasOwnProperty(query_string)) {var temp_var = query_strings_from_url[query_string].split('=', 2);vars[temp_var[0]] = decodeURI(temp_var[1]);}}var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];for (var css_class in css_selector_classes) {if (css_selector_classes.hasOwnProperty(css_class)) {var element = document.getElementsByClassName(css_selector_classes[css_class]);for (var j = 0; j < element.length; ++j) {element[j].textContent = vars[css_selector_classes[css_class]];}}}}</script></head><body style="border:0; margin: 0;" onload="subst()"><table style="border-bottom: 1px solid black; width: 100%"><tr><td class="section"></td><td style="text-align:right">Page <span class="page"></span> of <span class="topage"></span></td></tr></table></body></html>

四、前端服务

前端只需要将我们的html和css通过接口传给后端即可

try {const htmlContent = document.getElementById('report-content').outerHTML// 使用fetch API获取CSS文件const response = await fetch('../../assets/core-report.css')const css = await response.text()this.http.post('/generate-pdf',{content: htmlContent, // 网址或者HTML文档css,},undefined,{responseType: 'arraybuffer',observe: 'response',}).subscribe((response: any) => {if (!response) {this.dloading = falsethrow new Error('生成 PDF 失败')}this.downloadProgress = 100// 将 ArrayBuffer 转换为 Blob 对象const blob = new Blob([response.body], { type: 'application/pdf' })// 创建一个 URL 对象const url = URL.createObjectURL(blob)// 下载 PDF 文件const a = document.createElement('a')a.href = urla.download = `demo.pdf`document.body.appendChild(a)a.click()document.body.removeChild(a)URL.revokeObjectURL(url)},(error) => {console.error('PDF生成失败:', error)})} catch (error) {console.error('PDF生成失败:', error)}

我们通过脚本获取到html文档,通过fetch直接将文件内容获取,然后通过接口将两个参数传给后端,后端通过将两个内容组装成完整html,调用wkhtmltopdf,生成pdf,在通过文件流返回前端下载。这样生成的pdf,支持文本选中、复制、搜索,同时它会根据H标签识别页签导航内容,实现页签点击导航,YYDS!

注意点:

1:如果内容中存在canvas或者图片需要转base64传给后端,或者使用cdn链接

2:css3中的样式不支持,比如:阴影,以及flex布局不支持

3:内容被切分

在每个章节的标题或者其他地方我们往往不希望标题被切成两半,分别出现在两个页面当中。因此,我们需要添加如下样式:

.title {page-break-before: always;page-break-after: always;page-break-inside: avoid;
}

4: 表格切分

文档中会出现大量的表格。如果希望放置表格被切分也是同样的处理方式 

table tr {word-break: break-all;page-break-before: always;page-break-after: always;page-break-inside: avoid;
}

欢迎在评论区交流。

如果文章对你有所帮助,❤️关注+点赞❤️鼓励一下!博主会持续更新。。。。

往期回顾

 CSS多栏布局-两栏布局和三栏布局

 border边框影响布局解决方案

 css 设置字体渐变色和阴影

css 重置样式表(Normalize.css)

 css实现元素居中的6种方法 

Angular8升级至Angular13遇到的问题

前端vscode必备插件(强烈推荐)

Webpack性能优化

vite构建如何兼容低版本浏览器

前端性能优化9大策略(面试一网打尽)!

vue3.x使用prerender-spa-plugin预渲染达到SEO优化

 vite构建打包性能优化

 vue3.x使用prerender-spa-plugin预渲染达到SEO优化

 ES6实用的技巧和方法有哪些?

 css超出部分显示省略号

vue3使用i18n 实现国际化

vue3中使用prismjs或者highlight.js实现代码高亮

什么是 XSS 攻击?什么是 CSRF?什么是点击劫持?如何防御

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/895122.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

示例:JAVA调用deepseek

近日&#xff0c;国产AI DeepSeek在中国、美国的科技圈受到广泛关注&#xff0c;甚至被认为是大模型行业的最大“黑马”。在外网&#xff0c;DeepSeek被不少人称为“神秘的东方力量”。1月27日&#xff0c;DeepSeek应用登顶苹果美国地区应用商店免费APP下载排行榜&#xff0c;在…

.NET周刊【2月第1期 2025-02-02】

国内文章 dotnet 9 已知问题 默认开启 CET 导致进程崩溃 https://www.cnblogs.com/lindexi/p/18700406 本文记录 dotnet 9 的一个已知且当前已修问题。默认开启 CET 导致一些模块执行时触发崩溃。 dotnet 使用 ColorCode 做代码着色器 https://www.cnblogs.com/lindexi/p/…

AES200物理机部署DeepSeek-R1蒸馏模型

AES200物理机部署DeepSeek-R1模型 华为官方官宣自己的NPU支持DeepSeek-R1模型部署&#xff0c;华为的大模型推理部署依托于其大模型推理引擎&#xff1a;MindIE&#xff0c;但是根据MindIE的文档&#xff0c;其只支持以下硬件&#xff1a; 表1 MindIE支持的硬件列表 类型配置…

【后端开发】系统设计101——Devops,Git与CICD,云服务与云原生,Linux,安全性,案例研究(30张图详解)

【后端开发】系统设计101——Devops&#xff0c;Git与CICD&#xff0c;云服务与云原生&#xff0c;Linux&#xff0c;安全性&#xff0c;案例研究&#xff08;30张图详解&#xff09; 文章目录 1、DevopsDevOps与SRE与平台工程的区别是什么&#xff1f;什么是k8s&#xff08;Ku…

正泰中间电磁继电器【8脚10A】DC24V 待机功率

需求&#xff1a;继电器能耗测试。 1.连接24V2A的电源&#xff0c; 2. 稳定功率为 1.4W 3. 正泰中间电磁继电器【8脚10A】直流DC24V 注&#xff1a;联通时电磁继电器会轻微发热 4.电磁继电器的工作原理基于电流的磁效应 电磁激励&#xff1a;电磁继电器主要由线圈、铁芯、衔…

npm无法加载文件 因为此系统禁止运行脚本

安装nodejs后遇到问题&#xff1a; 在项目里【node -v】可以打印出来&#xff0c;【npm -v】打印不出来&#xff0c;显示npm无法加载文件 因为此系统禁止运行脚本。 但是在winr&#xff0c;cmd里【node -v】,【npm -v】都也可打印出来。 解决方法&#xff1a; cmd里可以打印出…

JVM春招快速学习指南

1.说在前面 在Java相关岗位的春/秋招面试过程中&#xff0c;JVM的学习是必不可少的。本文主要是通过《深入理解Java虚拟机》第三版来介绍JVM的学习路线和方法&#xff0c;并对没有过JVM基础的给出阅读和学习建议&#xff0c;尽可能更加快速高效的进行JVM的学习与秋招面试的备战…

认识Electron 开启新的探索世界一

一、Electron轻松入门 1.搭建开发环境&#xff1a; 一般情况下开发者会使用node.js来创建electron项目&#xff0c;node.js是一个基于Chrome V8引擎的javascript运行环境&#xff0c;所以首先需要到官网去下载安装node.js 下载链接&#xff1a;https://nodejs.org/enhttps://no…

MySQL下载过程

MySQL Enterprise Edition Downloads | Oracle mysql官方下载网址&#xff08;9.2版本&#xff09; 下面的示例是5.7的包&#xff0c;过程是一样的 port&#xff1a;3308&#xff08;默认的是3306&#xff0c;笔者下了一个占用了该端口&#xff09; root&#xff1a;123456 问题…

【学术投稿】第五届计算机网络安全与软件工程(CNSSE 2025)

重要信息 官网&#xff1a;www.cnsse.org 时间&#xff1a;2025年2月21-23日 地点&#xff1a;中国-青岛 简介 第五届计算机网络安全与软件工程&#xff08;CNSSE 2025&#xff09;将于2025年2月21-23日在中国-青岛举行。CNSSE 2025专注于计算机网络安全、软件工程、信号处…

Qt:QWidget核心属性

目录 QWidget核心属性 enab geometry WindowFrame的影响 windowTitle windowIcon qrc文件管理资源 windowOpacity cursor font toolTip focusPolicy styleSheet QWidget核心属性 在Qt中使用QWidget类表示"控件"&#xff0c;如按钮、视图、输入框、滚动…

Linux TCP 编程详解与实例

一、引言 在网络编程的领域中&#xff0c;TCP&#xff08;Transmission Control Protocol&#xff09;协议因其可靠的数据传输特性而被广泛应用。在 Linux 环境下&#xff0c;使用 C 或 C 进行 TCP 编程可以实现各种强大的网络应用。本文将深入探讨 Linux TCP 编程的各个方面&…

论文笔记:Rethinking Graph Neural Networks for Anomaly Detection

目录 摘要 “右移”现象 beta分布及其小波 实验 《Rethinking Graph Neural Networks for Anomaly Detection》&#xff0c;这是一篇关于图&#xff08;graph&#xff09;上异常节点诊断的论文。 论文出处&#xff1a;ICML 2022 论文地址&#xff1a;Rethinking Graph Ne…

神经网络常见激活函数 6-RReLU函数

文章目录 RReLU函数导函数函数和导函数图像优缺点pytorch中的RReLU函数tensorflow 中的RReLU函数 RReLU 随机修正线性单元&#xff1a;Randomized Leaky ReLU 函数导函数 RReLU函数 R R e L U { x x ≥ 0 a x x < 0 \rm RReLU \left\{ \begin{array}{} x \quad x \ge 0…

Vue(6)

一.路由板块封装 &#xff08;1&#xff09;路由的封装抽离 目标&#xff1a;将路由板块抽离出来 好处&#xff1a;拆分板块&#xff0c;利于维护 // 路由的使用步骤 5 2 // 5个基础步骤 // 1. 下载 v3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象…

【python】matplotlib(animation)

文章目录 1、matplotlib.animation1.1、FuncAnimation1.2、修改 matplotlib 背景 2、matplotlib imageio2.1、折线图2.2、条形图2.3、散点图 3、参考 1、matplotlib.animation 1.1、FuncAnimation matplotlib.animation.FuncAnimation 是 Matplotlib 库中用于创建动画的一个…

【东莞常平】戴尔R710服务器不开机维修分享

1&#xff1a;2025-02-06一位老客户的朋友刚开工公司ERP服务器一台戴尔老服务器故障无法开机&#xff0c;于是经老客户介绍找到我们。 2&#xff1a;服务器型号是DELL PowerEdge R710 这个服务器至少也有15年以上的使用年限了。 3&#xff1a;客户反馈的故障问题为&#xff1a;…

Spring AI -使用Spring快速开发ChatGPT应用

前言 Spring在Java生态中一直占据大半江山。最近我发现Spring社区推出了一个Spring AI项目&#xff0c;目前该项目还属于Spring实验性项目&#xff0c;但是我们可以通过该项目&#xff0c;可以非常快速的开发出GPT对话应用。 本篇文章将会对SpringAI进行简单的介绍和使用&#…

经典排序算法复习----C语言

经典排序算法复习 分类 交换类 冒泡快排 分配类 计数排序基数排序 选择类 选择排序 堆排序 归并类 归并排序 插入类 直接插入排序 希尔排序 折半插入排序 冒泡排序 基于交换。每一轮找最大值放到数组尾部 //冒泡排序 void bubSort(int* arr,int size){bool sorte…

BFS解决拓扑排序(3题)

目录 拓扑排序 1.如何排序&#xff1f; 2.如何形成拓扑排序 3.如何建图 1.看数据稠密度 2. 根据算法流程灵活建图 1.课程表 2.课程表2 3.火星词典 拓扑排序 找到做事情的先后顺序&#xff0c;拓扑排序的结果可能不是唯一的 1.如何排序&#xff1f; 1.找出图中入度为…