pdf-lib:优点是可以控制输出内容,缺点是麻烦
html2canvas:优点是直接把html页面转成图片之后插入pdf很方便,不用过多的代码,缺点是不好控制图片大小,容易被戒断,可以把想打印的内容藏在页面外面(postion:absoult;left:-9999)
原生打印:原生浏览器打印,完全无法控制
pdfkit:尝试了一下pdfkit,一直在报错,不好用目前只有0.12.1版本能打印,但是echarts好像也放不上去,格式要求很多(字体,图片)
依赖下载
npm包下载
npm install pdf-lib
npm install jspdf
npm install html2canvas
npm install echarts
npm install @pdf-lib/fontkit
字体下载(pdf-lib使用,ttf的会用不了,得用otf的格式,第二个链接)
https://github.com/adobe-fonts/source-han-sans/tree/release/OTC
https://github.com/adobe-fonts/source-han-sans/tree/release/SubsetOTF/CN
<template>
<div class="pdf-container">
<el-button type="primary" @click="downloadPdf">导出 PDF</el-button>
<el-button type="primary" @click="nativePrint">原生打印</el-button>
<el-button type="primary" @click="downloadPdfLib"
>导出 PDF(PDFKit)</el-button
>
<div ref="pdfContent" class="pdf-content">
<div class="pdf-page">
<div>测试</div>
<div>测试</div>
<div>
测试。
</div>
<div ref="chartRef" id="chart" style="width: 600px; height: 280px" />
<div>测试测试测试测试测试测试。</div>
<div>测试测试测试测试测试测试。</div>
<div>
测试测试测试测试测试测试。
</div>
<div ref="chartRef2" id="chart" style="width: 600px; height: 280px" />
</div>
<div class="pdf-page">
<div>测试测试测试测试测试测试。</div>
<div>
测试测试测试测试测试测试。
</div>
<div ref="chartRef3" id="chart" style="width: 600px; height: 280px" />
<div>测试测试测试测试测试测试。</div>
<div>测试测试测试测试测试测试。</div>
<div>
测试测试测试测试测试测试。
</div>
<div ref="chartRef4" id="chart" style="width: 600px; height: 280px" />
</div>
<div class="pdf-page">
<div>(测试测试测试测试测试测试。</div>
<div>
测试测试测试测试测试测试。
</div>
<div ref="chartRef5" id="chart" style="width: 600px; height: 280px" />
</div>
</div>
</div>
</template>
<script>
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
// import PDFDocument from "pdfkit"; // 新增
import "@fontsource/noto-sans-sc";
// const PDFDocument = require("pdfkit/js/pdfkit.standalone");
export default {
mounted() {
this.initChart();
},
methods: {
initChart() {
const chart = this.$echarts.init(this.$refs.chartRef, "", {
renderer: "svg", // 强制使用 SVG
devicePixelRatio: window.devicePixelRatio || 1, // 通常设为 2(Retina 屏)或更高,这样在页面上图片不会变糊了
});
chart.setOption({
title: { text: "示例 ECharts 图表" },
tooltip: {},
xAxis: { data: ["A", "B", "C", "D", "E"] },
yAxis: {},
series: [
{
name: "数量",
type: "bar",
data: [5, 20, 36, 10, 10],
},
],
});
const chart2 = this.$echarts.init(this.$refs.chartRef2, "", {
renderer: "svg", // 强制使用 SVG
devicePixelRatio: window.devicePixelRatio || 1, // 通常设为 2(Retina 屏)或更高,这样在页面上图片不会变糊了
});
chart2.setOption({
title: { text: "示例 ECharts 图表" },
tooltip: {},
xAxis: { data: ["A", "B", "C", "D", "E"] },
yAxis: {},
series: [
{
name: "数量",
type: "bar",
data: [5, 20, 36, 10, 10],
},
],
});
const chart3 = this.$echarts.init(this.$refs.chartRef3, "", {
renderer: "svg", // 强制使用 SVG
devicePixelRatio: window.devicePixelRatio || 1, // 通常设为 2(Retina 屏)或更高,这样在页面上图片不会变糊了
});
chart3.setOption({
title: { text: "示例 ECharts 图表" },
tooltip: {},
xAxis: { data: ["A", "B", "C", "D", "E"] },
yAxis: {},
series: [
{
name: "数量",
type: "bar",
data: [5, 20, 36, 10, 10],
},
],
});
const chart4 = this.$echarts.init(this.$refs.chartRef4, "", {
renderer: "svg", // 强制使用 SVG
devicePixelRatio: window.devicePixelRatio || 1, // 通常设为 2(Retina 屏)或更高,这样在页面上图片不会变糊了
});
chart4.setOption({
title: { text: "示例 ECharts 图表" },
tooltip: {},
xAxis: { data: ["A", "B", "C", "D", "E"] },
yAxis: {},
series: [
{
name: "数量",
type: "bar",
data: [5, 20, 36, 10, 10],
},
],
});
const chart5 = this.$echarts.init(this.$refs.chartRef5, "", {
renderer: "svg", // 强制使用 SVG
devicePixelRatio: window.devicePixelRatio || 1, // 通常设为 2(Retina 屏)或更高,这样在页面上图片不会变糊了
});
chart5.setOption({
title: { text: "示例 ECharts 图表" },
tooltip: {},
xAxis: { data: ["A", "B", "C", "D", "E"] },
yAxis: {},
series: [
{
name: "数量",
type: "bar",
data: [5, 20, 36, 10, 10],
},
],
});
},
downloadPdf() {
const content = this.$refs.pdfContent;
// 设置 A4 页面尺寸(宽度 210mm, 高度 297mm),以 pt 为单位(1mm ≈ 2.8346pt)
// 使用 html2canvas 渲染时保持原始内容
html2canvas(content, {
allowTaint: false, // 是否允许跨域图像。会污染画布,导致无法使用canvas.toDataURL 方法
backgroundColor: "#fff", // 画布背景色(如果未在DOM中指定)。设置null为透明
useCORS: true, // 是否尝试使用CORS从服务器加载图像
scale: 2, // 300 DPI = 3 × 96 DPI
dpi: 300,
}).then((canvas) => {
const imgData = canvas.toDataURL("image/png");
// 创建 jsPDF 实例并设置页面尺寸为 A4
const pdf = new jsPDF("p", "px", "a4"); // 直接修改页面宽高
// 获取页面宽高
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pageWidth; // 设置图片宽度为 PDF 页面宽度
const imgHeight = (canvas.height * imgWidth) / canvas.width;
console.log(canvas, "canvas");
console.log(pdf.internal.pageSize, "pdf.internal.pageSize");
console.log(pageHeight, "pageHeight");
console.log(imgHeight, "imgHeight");
let heightLeft = imgHeight;
let position = 0;
let pageNum = 1;
const totalPages = Math.ceil(imgHeight / pageHeight);
while (heightLeft > 0) {
if (pageNum > 1) pdf.addPage();
// 关键修复:跳过微小残余高度
if (heightLeft < 5) break;
const posY = -(pageHeight * (pageNum - 1));
pdf.addImage(imgData, "PNG", 0, posY, imgWidth, imgHeight);
// 页码(保持原样)
pdf.setFontSize(10);
pdf.setTextColor(150);
pdf.text(`${pageNum}/${totalPages}`, pageWidth / 2, pageHeight - 20, {
align: "center",
});
heightLeft -= pageHeight;
pageNum++;
}
pdf.save("export.pdf");
});
},
nativePrint() {
// 1. 克隆打印内容(避免影响原DOM)
const printContent = this.$refs.pdfContent.cloneNode(true);
// 2. 创建隐藏的打印容器
const printContainer = document.createElement("div");
printContainer.id = "print-container";
printContainer.appendChild(printContent);
// 3. 添加到DOM(隐藏状态)
document.body.appendChild(printContainer);
// 4. 调用打印
window.print();
// 5. 打印后移除临时容器
document.body.removeChild(printContainer);
},
async downloadPdfLib() {
const { PDFDocument, rgb } = await import("pdf-lib");
const fontkit = await import("@pdf-lib/fontkit");
const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit.default);
const fontBytes = await fetch("/SourceHanSansCN-Regular.otf").then(
(res) => res.arrayBuffer()
);
const customFont = await pdfDoc.embedFont(fontBytes);
const fontBytesBlod = await fetch("/SourceHanSansCN-Bold.otf").then(
(res) => res.arrayBuffer()
);
const customFontBlod = await pdfDoc.embedFont(fontBytesBlod);
const pageWidth = 595.28;
const pageHeight = 841.89;
const marginLeft = 50;
const marginTop = pageHeight - 50;
const fontSize = 12;
const lineHeight = fontSize * 1.5;
const maxTextWidth = pageWidth - marginLeft * 2;
function splitTextToLines(text, font, size, maxWidth) {
let lines = [];
let currentLine = "";
for (let i = 0; i < text.length; i++) {
const testLine = currentLine + text[i];
const width = font.widthOfTextAtSize(testLine, size);
if (width < maxWidth) {
currentLine = testLine;
} else {
if (currentLine) {
lines.push(currentLine);
}
currentLine = text[i];
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
function drawTextBlock(
page,
text,
x,
yStart,
alignCenter = false,
options = {}
) {
const {
fontSize = 12,
lineHeight = fontSize * 1.5,
color = rgb(0, 0, 0),
font = customFont,
} = options;
const lines = splitTextToLines(text, font, fontSize, maxTextWidth);
let y = yStart;
for (const line of lines) {
let drawX = x;
if (alignCenter) {
const textWidth = font.widthOfTextAtSize(line, fontSize);
drawX = (pageWidth - textWidth) / 2;
}
page.drawText(line, {
x: drawX,
y,
size: fontSize,
font,
color,
});
y -= lineHeight;
}
return y;
}
function addPageNumber(page, pageNumber, totalPages) {
const fontSize = 10;
const margin = 20;
const pageNumberText = `${pageNumber} / ${totalPages}`;
const textWidth = customFont.widthOfTextAtSize(
pageNumberText,
fontSize
);
page.drawText(pageNumberText, {
x: (pageWidth - textWidth) / 2,
y: margin,
size: fontSize,
font: customFont,
color: rgb(0, 0, 0),
});
}
let _this = this;
async function drawImage(page, base64Img, x, y, width, height) {
const canvas = await _this.convertSvgToCanvas(base64Img, 3);
const imgData = canvas.toDataURL("image/png");
const imgBytes = await fetch(imgData).then((res) => res.arrayBuffer());
const pngImage = await pdfDoc.embedPng(imgBytes);
page.drawImage(pngImage, {
x,
y: y - height,
width,
height,
});
}
const generateDirectory = true;
let y = 0;
let directoryPage;
if (generateDirectory) {
directoryPage = pdfDoc.addPage([pageWidth, pageHeight]);
let y = pageHeight - 100;
directoryPage.drawText("目录", {
x: marginLeft,
y: y,
size: 16,
font: customFont,
color: rgb(0, 0, 0),
});
y -= lineHeight;
directoryPage.drawText(
"一、测试测试测试测试测试测试。 .......................................... 1",
{
x: marginLeft,
y: y,
size: fontSize,
font: customFont,
color: rgb(0, 0, 0),
}
);
y -= lineHeight;
directoryPage.drawText(
"二、测试测试测试测试测试测试。 .......................................... 2",
{
x: marginLeft,
y: y,
size: fontSize,
font: customFont,
color: rgb(0, 0, 0),
}
);
}
const page1 = pdfDoc.addPage([pageWidth, pageHeight]);
y = marginTop;
y = drawTextBlock(
page1,
"测试测试测试测试测试测试。",
marginLeft,
y,
true,
{ fontSize: 16, font: customFontBlod }
);
y -= lineHeight / 2;
y = drawTextBlock(page1, "一、贸易总体情况", marginLeft, y, false, {
fontSize: 14,
});
y -= lineHeight / 2;
y = drawTextBlock(
page1,
" 测试测试测试测试测试测试。",
marginLeft,
y
);
const chart1 = this.$echarts.getInstanceByDom(this.$refs.chartRef);
const svgData1 = chart1.getDataURL({ type: "svg" });
y -= lineHeight * 1.5;
await drawImage(page1, svgData1, marginLeft, y, 495, 250);
y -= 250 + lineHeight;
y = drawTextBlock(page1, "二、产品结构情况", marginLeft, y, false, {
fontSize: 14,
});
y -= lineHeight / 2;
y = drawTextBlock(page1, "(一)出口商品结构", marginLeft, y);
y -= lineHeight / 2;
y = drawTextBlock(
page1,
"测试测试测试测试测试测试。",
marginLeft,
y
);
y -= lineHeight * 1.5;
const chart2 = this.$echarts.getInstanceByDom(this.$refs.chartRef2);
const svgData2 = chart2.getDataURL({ type: "svg" });
await drawImage(page1, svgData2, marginLeft, y, 495, 250);
addPageNumber(page1, 1, 3);
const page2 = pdfDoc.addPage([pageWidth, pageHeight]);
y = marginTop;
y = drawTextBlock(page2, "(二)进口商品结构", marginLeft, y);
y -= lineHeight / 2;
y = drawTextBlock(
page2,
"测试测试测试测试测试测试。",
marginLeft,
y
);
const chart3 = this.$echarts.getInstanceByDom(this.$refs.chartRef3);
const svgData3 = chart3.getDataURL({ type: "svg" });
y -= lineHeight * 1.5;
await drawImage(page2, svgData3, marginLeft, y, 495, 250);
y -= 250 + lineHeight;
y = drawTextBlock(page2, "测试测试测试测试测试测试。", marginLeft, y, false, {
fontSize: 14,
});
y -= lineHeight / 2;
y = drawTextBlock(page2, "测试测试测试测试测试测试。", marginLeft, y);
y -= lineHeight / 2;
y = drawTextBlock(
page2,
"测试测试测试测试测试测试。",
marginLeft,
y,
false,
{
fontSize: 12,
color: rgb(0.2, 0.2, 0.6),
lineHeight: 20,
}
);
const chart4 = this.$echarts.getInstanceByDom(this.$refs.chartRef4);
const svgData4 = chart4.getDataURL({ type: "svg" });
y -= lineHeight * 1.5;
await drawImage(page2, svgData4, marginLeft, y, 495, 250);
addPageNumber(page2, 2, 3);
const page3 = pdfDoc.addPage([pageWidth, pageHeight]);
y = marginTop;
y = drawTextBlock(page3, "(二)进口商品结构", marginLeft, y);
y -= lineHeight / 2;
y = drawTextBlock(
page3,
"测试测试测试测试测试测试。",
marginLeft,
y
);
const chart5 = this.$echarts.getInstanceByDom(this.$refs.chartRef5);
const svgData5 = chart5.getDataURL({ type: "svg" });
y -= lineHeight * 1.5;
await drawImage(page3, svgData5, marginLeft, y, 495, 250);
addPageNumber(page3, 3, 3);
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "测试测试测试测试测试测试.pdf";
link.click();
},
// 将文本分割为多行
splitTextIntoLines(text, maxWidth, font, fontSize) {
const lines = [];
let line = "";
for (let i = 0; i < text.length; i++) {
const testLine = line + text[i];
const width = font.widthOfTextAtSize(testLine, fontSize);
if (width < maxWidth) {
line = testLine;
} else {
lines.push(line);
line = text[i];
}
}
if (line) lines.push(line);
return lines;
},
// 使用 html2canvas 将 SVG 转换为 Canvas
convertSvgToCanvas(svgData, scale = 4) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = svgData;
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// 设置canvas尺寸,使用scale来增加渲染分辨率
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.scale(scale, scale); // 拉伸canvas,提升图像分辨率
ctx.drawImage(img, 0, 0);
resolve(canvas);
};
img.onerror = reject;
});
},
},
};
</script>
<style scoped>
.pdf-container {
background: white;
padding: 20px;
}
.pdf-content {
width: 794px;
background: #fff;
/* padding: 20px; */
/* margin-top: 20px; */
/* border: 1px solid #eee; */
}
.pdf-page {
height: 1123px;
/* border: 1px solid #eee; */
/* margin: 10px 0; */
padding: 20px;
}
.pdf-page:last-child {
height: 1100px;
}
</style>