把网页导出成 PDF 本质上就是把 html 代码(包括 css 样式)转换成 PDF
用浏览器的 api 生成 | 用各类工具库把 html 生成 PDF | |
---|---|---|
在前端生成 | 1. 直接调用 print 事件 <br /> 2. 把 html 代码添加进一个隐藏的 iframe 标签里,然后调用 iframe 标签的 print 事件 | 1. 先通过 html2canvas 把网页转换成图片,再用 jsPDF 来生成 PDF <br /> 2. 只使用 jsPDF 来生成 PDF |
在后台生成 | 1. 直接用 headless 浏览器的命令行生成 <br /> 2. 用 Playwright 这类工具操作 headless 浏览器生成 | 用 spipu/html2pdf 这类库解释 html 后生成 PDF |
在后台生成,能保持一致的样式,且不需要浏览器支持,但会占用服务器资源。生成速度大概率会比在前端慢。
在前端生成,各个浏览器生成的 PDF 样式可能会有一点差异,但不占用服务器资源。如果是调用 print 事件方式生成的,速度肯定比在后台生成快。
<!-- 实现难度 样式一致 a 标签 缺点 -->(function(){
let printCode = `<h1>test</h1>`;
var iframe = document.createElement("iframe");
iframe.id = 'iframe_print_' + Math.round(new Date().getTime());
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.contentWindow.document.write(printCode); // 一定要用 write 方法, innerHTML 属性有时会打印失败
iframe.contentWindow.focus();
setTimeout(function() {
// 如果太快调用 print 方法,可能会因为节点未渲染完而导致pdf一片空白
iframe.contentWindow.print();
setTimeout(function() {
// 如果还没渲染完pdf就移除 iframe 的标签,会打印失败,等待的时间可以调整,甚至一直留着 iframe 标签都可以
document.getElementById(iframe.id).parentNode.removeChild(iframe);
}, 3000);
}, 1000);
})();
把 markdown 转换成 html,再把html转换成 pdf
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<div>
<textarea id="content"># test</textarea>
<button id="convert">convert</button>
</div>
<script>
document.getElementById('convert').addEventListener('click', function() {
let printCode = marked.marked(document.getElementById('content').value);
var iframe = document.createElement("iframe");
iframe.id = 'iframe_print_' + Math.round(new Date().getTime());
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.contentWindow.document.write(printCode); // 一定要用 write 方法, innerHTML 属性有时会打印失败
iframe.contentWindow.focus();
setTimeout(function() {
// 如果太快调用 print 方法,可能会因为节点未渲染完而导致pdf一片空白
iframe.contentWindow.print();
setTimeout(function() {
// 如果还没渲染完pdf就移除 iframe 的标签,会打印失败,等待的时间可以调整,甚至一直留着 iframe 标签都可以
document.getElementById(iframe.id).parentNode.removeChild(iframe);
}, 3000);
}, 1000);
});
</script>
</body>
</html>
这是先生成图片再生成pdf
(function(){
function addScript(src) {
tmpNode = document.createElement("script");
tmpNode.src = src;
document.body.appendChild(tmpNode);
}
addScript("https://cdn.bootcdn.net/ajax/libs/dom-to-image/2.6.0/dom-to-image.js");
addScript("https://cdn.bootcdn.net/ajax/libs/jspdf/2.1.1/jspdf.umd.js");
setTimeout(()=>{
domtoimage.toJpeg(document.body, { quality: 0.95, bgcolor: '#FFFFFF' })
.then(function (dataUrl) {
var image = new Image();
image.onload = function(){
var contentWidth = image.width;
var contentHeight = image.height;
// 这是生成横向 A4
// 一页pdf显示html页面生成的canvas高度;
var pageHeight = 592.28;
// 未生成pdf的html页面高度
var leftHeight = contentHeight*(1-0.138);
//pdf页面偏移
var position = 0;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
var imgWidth = 841.89;
var imgHeight = 841.89/contentWidth * contentHeight;
// 这是生成纵向 A4
// //一页pdf显示html页面生成的canvas高度;
// var pageHeight = contentWidth / 592.28 * 841.89;
// //未生成pdf的html页面高度
// var leftHeight = contentHeight;
// //pdf页面偏移
// var position = 0;
// //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
// var imgWidth = 595.28;
// var imgHeight = 595.28/contentWidth * contentHeight;
// var pageData = canvas.toDataURL('image/png', 1.0);
const { jsPDF } = window.jspdf;
// var pdf = new jsPDF('', 'pt', 'a4');
var pdf = new jsPDF('landscape', 'pt', 'a4');
//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
//当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
pdf.addImage(image, 'JPEG', 0, 0, imgWidth, imgHeight );
} else {
while(leftHeight > 0) {
pdf.addImage(image, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= 592.28;
console.log(leftHeight, position);
//避免添加空白页
if(leftHeight > 0) {
pdf.addPage();
}
}
}
pdf.save('content.pdf');
};
image.src = dataUrl;
});
}, 3000)
})();
// 用于应对 CSP ,一些情况下,通过控制台无法引用外部的js链接
(function(){
// 检查是否存在 head 标签
if (!document.head) {
// 创建一个新的 head 标签
var head = document.createElement('head');
// 将新创建的 head 标签插入到 html 标签中
document.documentElement.insertBefore(head, document.documentElement.firstChild);
}
var meta = document.createElement('meta');
meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute('content', 'script-src https://cdn.bootcdn.net');
// 将新创建的 meta 标签插入到 head 标签中
document.getElementsByTagName('head')[0].appendChild(meta);
})();
// 如果一些资源无法加载,例如 谷歌字体 这种,那么图片可能会生成失败
(function(){
function addScript(src) {
tmpNode = document.createElement("script");
tmpNode.src = src;
document.body.appendChild(tmpNode);
}
addScript("https://cdn.bootcdn.net/ajax/libs/dom-to-image/2.6.0/dom-to-image.js");
setTimeout(()=> {
domtoimage.toJpeg(document.body, { quality: 0.95, bgcolor: '#FFFFFF' })
.then(function (dataUrl) {
var link = document.createElement('a');
link.download = 'image-' + Date.now() + '.jpeg';
link.href = dataUrl; // 这是 base64 的字符串,好像直接用那种 blob 对象也可以
link.click();
});
}, 3000)
})();
"C:\Users\a\AppData\Local\Google\Chrome\Application\chrome.exe" \
--headless=new \
--no-sandbox \
--disable-gpu \
--window-size=1920,1080 \
--ignore-certificate-errors \
--no-pdf-header-footer \
--print-to-pdf=f2h2h1.pdf \
"https://f2h2h1.github.io/"
--headless:启用无头模式,无需 GUI 环境即可运行。
--no-sandbox:禁用沙盒模式,某些环境下可能需要此选项以避免权限问题。
--disable-gpu:禁用 GPU 硬件加速,有助于在不支持 GPU 或驱动有问题的系统上提高稳定性。
--window-size:设置浏览器窗口大小,这对于页面布局可能很重要。
--print-to-pdf:指定输出的 PDF 文件名和路径。
--incognito:使用无痕模式打开页面
--ignore-certificate-errors 忽略证书错误
--timeout 定义了最长等待时间(以毫秒为单位)
--no-pdf-header-footer 不要页眉 不要页脚
最后一个参数是需要导出为 PDF 的网页 URL。
--headless=new
这个参数可以用新的headless模式,生成的pdf能保留更多的css样式。但笔者认为还是旧模式生成的pdf好看一点
--virtual-time-budget
在某种程度上对于任何具有时效性的代码(例如,setTimeout/setInterval),虚拟时间可充当“快进”。
它会强制浏览器尽快执行相应网页的任何代码,同时让网页相信时间实际上会经过。
下面展示了如何捕获网页在 42 秒后的状态并将其保存为 PDF 格式:
chrome --headless=new --print-to-pdf --virtual-time-budget=42000 https://mathiasbynens.be/demo/time
导出 html 代码或截图,就把 --print-to-pdf 替换成 --dump-dom 或 --screenshot 。 如果pdf输出的路径不是绝对路径,那么pdf可能会生成在chrome 的安装根目录下, 要确保浏览器在对应的目录有写入的权限。 edge 也可以用类似的命令,但火狐却没有生成PDF的命令
import time
from playwright.sync_api import sync_playwright, Playwright
def run(playwright: Playwright):
chromium = playwright.chromium
browser = chromium.launch(devtools=True, headless=False, executable_path="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
# 其它一些可能会用到的参数
# args=args, executable_path=executablePath, devtools=devtools, headless=headless, user_data_dir='./playwright_temp/user', user_agent=ua, viewport=windowSize, is_mobile=isMobile
page = browser.new_page()
targetUrl = "https://f2h2h1.github.io/article/%E6%8A%8A%E7%BD%91%E9%A1%B5%E5%AF%BC%E5%87%BA%E6%88%90PDF.html"
waittime = 3
load_script = '''
console.log(123);
'''
if load_script != None:
page.on("load", lambda :page.evaluate(load_script))
page.goto(targetUrl)
page.wait_for_load_state('load')
time.sleep(waittime)
# page.pdf(path="page.pdf")
with open('page.pdf', 'wb') as file:
file.write(page.pdf())
browser.close()
if __name__ == '__main__':
with sync_playwright() as playwright:
run(playwright)
Opera 浏览器(77.0.4054.203)有一个把页面另存为 PDF 的功能(不是打印预览),几乎可以把页面的样式完整地保留下来(不是打印的样式就是当前渲染的样式)而且还能保持 a 标签的链接。但只能通过图形界面操作,没有命令行参数,也不能通过 Playwright 这类工具来操作浏览器生成。 可以弄一个单独的 Windows 服务器,用 autoit 这类工具操作 Opera 浏览器把页面另存为 PDF 。
Adobe Acrobat 的浏览器扩展也可以生成 PDF ,但这个扩展似乎需要在系统里安装 Adobe Acrobat , Adobe Acrobat 似乎没有免费的版本,虽然破解版也不是不能用,但商业使用的话始终有风险
save as pdf 和 pirnt to pdf 是不一样的, 具体区别还不清楚,其中一个区别是 save as pdf 里的文字是可以选中的, save as pdf 可以保留链接
thead 标签可以在表格被分页时,每一页都保持一个表头
<!-- 右键菜单的打印 ctrl+p 浏览器菜单的打印 直接调用 print 事件 这四种操作生成的pdf效果是一样的 有哪些可以导出pdf的浏览器插件 其实可以把上面几段js写成Tampermonkey脚本吧 看上去挺厉害的一个工具,用于把网页保存为pdf,但本质上依然是用浏览器实现的,这是用 Qt WebKit https://wkhtmltopdf.org/ https://github.com/wkhtmltopdf/wkhtmltopdf 因为用了 qt 所以可以跨平台运行 但从 github 的仓库来看,好像已经处于不活跃的阶段了 我也写一个,封装一个浏览器在里面,然后做到开箱即用 其实可以不单输出 pdf ,输出 图片 html 代码也是可以的吧 把那个 seo 的仓库再改一下 面向一般用户的,面向开发者的 -->