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

news2025/2/7 13:30:24

目录

需求

一、方案调研

二、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 = false
              throw 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 = url
            a.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.coloradmin.cn/o/2294311.html

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

相关文章

deepseek本地部署

DeepSeek本地部署详细指南 DeepSeek作为一款开源且性能强大的大语言模型&#xff0c;提供了灵活的本地部署方案&#xff0c;让用户能够在本地环境中高效运行模型&#xff0c;同时保护数据隐私&#xff0c;这里记录自己DeepSeek本地部署流程。 主机环境 cpu:amd 7500Fgpu:406…

【Reading Notes】Favorite Articles from 2025

文章目录 1、January2、February3、March4、April5、May6、June7、July8、August9、September10、October11、November12、December 1、January 极越之后&#xff0c;中国车市只会倒下更多人&#xff08;2025年01月01日&#xff09; 在这波枪林弹雨中&#xff0c;合资品牌中最…

配置@别名路径,把@/ 解析为 src/

路径解析配置 webpack 安装 craco npm i -D craco/craco 项目根目录下创建文件 craco.config.js &#xff0c;内容如下 const path require(path) module.exports {webpack: {// 配置别名alias: {// 约定&#xff1a; 使用 表示src文件所在路径: path.resolve(__dirname,src)…

AJAX笔记原理篇

黑马程序员视频地址&#xff1a; AJAX-Day03-01.XMLHttpRequest_基本使用https://www.bilibili.com/video/BV1MN411y7pw?vd_source0a2d366696f87e241adc64419bf12cab&spm_id_from333.788.videopod.episodes&p33https://www.bilibili.com/video/BV1MN411y7pw?vd_sour…

康谋方案 | BEV感知技术:多相机数据采集与高精度时间同步方案

随着自动驾驶技术的快速发展&#xff0c;车辆准确感知周围环境的能力变得至关重要。BEV&#xff08;Birds-Eye-View&#xff0c;鸟瞰图&#xff09;感知技术&#xff0c;以其独特的视角和强大的数据处理能力&#xff0c;正成为自动驾驶领域的一大研究热点。 一、BEV感知技术概…

更换IP属地会影响网络连接速度吗

在数字化时代&#xff0c;网络连接速度对于个人用户和企业来说都至关重要。无论是日常浏览网页、观看视频&#xff0c;还是进行在线办公、游戏娱乐&#xff0c;网络速度都直接影响着我们的体验。而IP属地&#xff0c;作为网络连接中的一个重要元素&#xff0c;其变动是否会引发…

深入探索 C++17 特征变量模板 (xxx_v)

文章目录 一、C++类型特征的前世今生二、C++17特征变量模板闪亮登场三、常见特征变量模板的实际应用(一)基本类型判断(二)指针与引用判断四、在模板元编程中的关键作用五、总结与展望在C++的持续演进中,C++17带来了许多令人眼前一亮的特性,其中特征变量模板(xxx_v)以其…

无用知识研究:std::initializer_list的秘密

先说结论&#xff0c;用std::initializer_list初始化vector&#xff0c;内部逻辑是先生成了一个临时数组&#xff0c;进行了拷贝构造&#xff0c;然后用这个数组的起终指针初始化initializer_list。然后再用initializer_list对vector进行初始化&#xff0c;这个动作又触发了拷贝…

web安全:任意文件下载漏洞

背景&#xff1a; 点击对应名字&#xff0c;下载对应图片。但服务器还存在其他文件&#xff0c;只是前端没有展示出来。通过模拟路径下载&#xff0c;可以获取到意想不到的数据。 看点击代码&#xff1a; 如果模拟没有前端的图片&#xff0c;也会发现下载了 所以这个叫任…

oracle:索引(B树索引,位图索引,分区索引,主键索引,唯一索引,联合索引/组合索引,函数索引)

索引通过存储列的排序值来加快对表中数据的访问速度&#xff0c;帮助数据库系统快速定位到所需数据&#xff0c;避免全表扫描 B树索引(B-Tree Index) B树索引是一种平衡树结构&#xff0c;适合处理范围查询和精确查找。它的设计目标是保持数据有序&#xff0c;并支持高效的插入…

【CPP】CPP经典面试题

文章目录 引言1. C 基础1.1 C 中的 const 关键字1.2 C 中的 static 关键字 2. 内存管理2.1 C 中的 new 和 delete2.2 内存泄漏 3. 面向对象编程3.1 继承和多态3.2 多重继承 4. 模板和泛型编程4.1 函数模板4.2 类模板 5. STL 和标准库5.1 容器5.2 迭代器 6. 高级特性6.1 移动语义…

C++11详解(三) -- 可变参数模版和lambda

文章目录 1.可变模版参数1.1 基本语法及其原理1.2 包扩展1.3 empalce系列接口1.3.1 push_back和emplace_back1.3.2 emplace_back在list中的使用&#xff08;模拟实现&#xff09; 2. lambda2.1 lambda表达式语法 1.可变模版参数 1.1 基本语法及其原理 1. C11支持可变参数模版&…

网站打开提示不安全

当网站打开时显示“不安全”提示&#xff08;通常表现为浏览器地址栏中出现“不安全”字样或红色警告图标&#xff09;&#xff0c;这意味着网站未使用有效的SSL证书或HTTPS协议&#xff0c;导致浏览器认为连接不安全。以下是解决这一问题的详细步骤&#xff1a; 一. 原因分析 …

OpenCV:特征检测总结

目录 一、什么是特征检测&#xff1f; 二、OpenCV 中的常见特征检测方法 1. Harris 角点检测 2. Shi-Tomasi 角点检测 3. Canny 边缘检测 4. SIFT&#xff08;尺度不变特征变换&#xff09; 5. ORB 三、特征检测的应用场景 1. 图像匹配 2. 运动检测 3. 自动驾驶 4.…

python学opencv|读取图像(五十七)使用cv2.bilateralFilter()函数实现图像像素双边滤波处理

【1】引言 前序学习过程中&#xff0c;已经掌握了对图像的基本滤波操作技巧&#xff0c;具体的图像滤波方式包括均值滤波、中值滤波和高斯滤波&#xff0c;相关文章链接有&#xff1a; python学opencv|读取图像&#xff08;五十四&#xff09;使用cv2.blur()函数实现图像像素…

【SQL技术】不同数据库引擎 SQL 优化方案剖析

一、引言 在数据处理和分析的世界里&#xff0c;SQL 是不可或缺的工具。不同的数据库系统&#xff0c;如 MySQL、PostgreSQL&#xff08;PG&#xff09;、Doris 和 Hive&#xff0c;在架构和性能特点上存在差异&#xff0c;因此针对它们的 SQL 优化策略也各有不同。这些数据库中…

链式结构二叉树(递归暴力美学)

文章目录 1. 链式结构二叉树1.1 二叉树创建 2. 前中后序遍历2.1 遍历规则2.2 代码实现图文理解 3. 结点个数以及高度等二叉树结点个数正确做法&#xff1a; 4. 层序遍历5. 判断是否完全二叉树 1. 链式结构二叉树 完成了顺序结构二叉树的代码实现&#xff0c;可以知道其底层结构…

技术文档管理最佳实践:高效、专业、可持续

文章目录 技术文档管理最佳实践&#xff1a;高效、专业、可持续1. 技术文档的核心价值1.1 降低知识流失风险1.2 提升开发效率1.3 增强团队协作1.4 规范技术资产管理 2. 技术文档分类与规范2.1 代码相关文档2.2 过程与运维文档2.3 知识与培训文档 3. 工具选型&#xff1a;自动化…

56. Uboot移植实验

一、NXP官方Uboot编译与测试 1、将NXP提供的uboot拷贝到ubuntu中。 一个开发板也好运行uboot&#xff0c;DDR或者叫DRAM&#xff0c;串口&#xff0c;SD、EMMC、NAND。板子能工作。 测似结果&#xff1a; 1、uboot能正常启动 2、LCD驱动要根据所使用的屏幕修改。 3、NET初始…

AI大模型:本地部署deepseek

一、安装lmstudio 1、下载网站&#xff1a; LM Studio - Discover, download, and run local LLMs 2、直接安装即可&#xff0c;记住安装的路径 二、下载deepseek模型 2.1、下载的流程 1、下载网站 https://huggingface.co/models 2、在搜索框输入&#xff1a;deepseek …