前端PDF导出完全指南:JSPDF与HTML2Canvas深度解析与实战(上)

前端PDF导出完全指南:JSPDF与HTML2Canvas深度解析与实战(上)

前言:为什么需要前端PDF导出?

在现代Web开发中,将网页内容导出为PDF是一项常见但颇具挑战的需求。无论是生成报告、发票、合同还是数据可视化图表,前端PDF导出都能为用户提供便捷的离线查看和打印体验。传统的PDF生成通常需要在后端完成,但随着前端技术的发展,现在我们可以直接在浏览器中实现高质量的PDF导出功能。

本文将深入探讨两个核心工具——JSPDF和HTML2Canvas,通过详细的原理分析、实战案例和最佳实践,帮助您掌握前端PDF导出的核心技术。

第一部分:工具介绍与技术选型

1.1 JSPDF:轻量级PDF生成库

JSPDF是一个纯JavaScript实现的PDF生成库,它不依赖任何服务器端组件,完全在客户端运行。这个库最初发布于2010年,经过多年的发展,已经成为前端PDF生成的事实标准。

主要特性:
  • 纯客户端运行:不需要服务器支持
  • 轻量级:压缩后仅约60KB
  • 丰富的API:支持文本、图片、形状、字体等
  • 多语言支持:包括中文在内的多种语言
  • 插件系统:可通过插件扩展功能
基本使用:

javascript

// 最简单的PDF生成示例 const doc = new jsPDF(); // 添加文本 doc.text('Hello World!', 10, 10); // 保存PDF doc.save('document.pdf');

1.2 HTML2Canvas:网页截图神器

HTML2Canvas是一个强大的JavaScript库,它可以将HTML元素渲染为Canvas。虽然名字中包含"canvas",但它实际上是通过模拟浏览器渲染引擎来实现HTML到Canvas的转换。

工作原理:
  1. 解析目标元素的CSS样式
  2. 克隆DOM节点并应用样式
  3. 使用Canvas 2D API绘制每个节点
  4. 处理图片、渐变、阴影等复杂样式
技术特点:
  • 基于Canvas:输出为标准Canvas元素
  • 样式支持:支持大部分CSS属性
  • 跨域处理:可以配置跨域图片加载
  • 异步处理:使用Promise API

1.3 为什么选择这两个库?

组合优势

  • JSPDF擅长PDF操作:创建、编辑、保存PDF文档
  • HTML2Canvas擅长网页渲染:准确捕获网页视觉状态
  • 完美互补:HTML2Canvas生成图片,JSPDF将图片转为PDF

适用场景对比

场景推荐方案理由
简单文本PDF纯JSPDF轻量、快速、代码简洁
表格报表JSPDF + 表格插件结构化数据友好
复杂网页截图HTML2Canvas + JSPDF保留视觉样式
大量数据导出后端生成性能更好

第二部分:JSPDF深度解析

2.1 核心API详解

2.1.1 初始化与页面设置
// 创建PDF实例 const doc = new jsPDF({ orientation: 'p', // 方向:p-纵向,l-横向 unit: 'mm', // 单位:pt, mm, cm, in format: 'a4', // 格式:a3, a4, a5, letter等 compress: true, // 是否压缩 precision: 16 // 浮点数精度 }); // 页面尺寸获取 const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); // 添加新页面 doc.addPage(); // 删除页面 doc.deletePage(2); // 设置页面背景色 doc.setFillColor(240, 240, 240); doc.rect(0, 0, pageWidth, pageHeight, 'F');
2.1.2 文本处理
// 基本文本设置 doc.setFont("helvetica"); // 字体 doc.setFontSize(16); // 字号 doc.setTextColor(0, 0, 0); // 颜色 doc.setFontStyle("bold"); // 样式:normal, bold, italic, bolditalic // 添加文本 doc.text("单行文本", x, y); doc.text("多行文本", x, y, { maxWidth: 100, // 最大宽度 align: 'left', // 对齐:left, center, right baseline: 'top' // 基线:top, middle, bottom }); // 自动换行文本 const lines = doc.splitTextToSize( "这是一个很长的文本,需要自动换行显示", pageWidth - 40 ); doc.text(lines, 20, 30); // 多行文本带行高 const text = "第一行\n第二行\n第三行"; doc.text(text, 20, 50, { lineHeightFactor: 1.5 }); // 旋转文本 doc.textWithRotation("旋转文本", 100, 100, 45); // 旋转45度
2.1.3 中文支持
// 添加中文字体 // 1. 首先需要加载字体文件 const font = 'AAEAAAAQAQAABAAAR0RFRgE...'; // Base64编码的字体 // 2. 添加到JSPDF doc.addFileToVFS('chinese-normal.ttf', font); doc.addFont('chinese-normal.ttf', 'chinese', 'normal'); // 3. 使用中文字体 doc.setFont('chinese'); doc.text('中文字体测试', 20, 20); // 或者使用内置的亚洲字体 doc.setFont('simhei'); doc.setFontSize(16); doc.text('使用黑体显示中文', 20, 40);
2.1.4 图片处理
// 添加图片 const imgData = 'data:image/png;base64,iVBORw0KGgo...'; doc.addImage(imgData, 'PNG', 15, 40, 180, 160); // 完整参数 doc.addImage({ imageData: imgData, x: 15, y: 40, width: 180, height: 160, compression: 'FAST', // 压缩等级:NONE, FAST, MEDIUM, SLOW rotation: 0, // 旋转角度 alias: 'myImage', // 图片别名 format: 'PNG' // 格式:JPEG, PNG }); // 获取图片属性 const imgProps = doc.getImageProperties(imgData); console.log(`图片尺寸: ${imgProps.width}x${imgProps.height}`); // 图片缩放模式 const scaleToFit = (imgWidth, imgHeight, maxWidth, maxHeight) => { const widthRatio = maxWidth / imgWidth; const heightRatio = maxHeight / imgHeight; const ratio = Math.min(widthRatio, heightRatio); return { width: imgWidth * ratio, height: imgHeight * ratio }; };
2.1.5 图形绘制
// 线条 doc.setLineWidth(0.5); // 线宽 doc.setDrawColor(0, 0, 255); // 线条颜色 doc.line(20, 20, 100, 20); // 直线 // 矩形 doc.setFillColor(255, 0, 0); // 填充色 doc.rect(20, 30, 50, 30, 'F'); // 填充矩形 doc.rect(80, 30, 50, 30, 'S'); // 描边矩形 doc.rect(140, 30, 50, 30, 'FD'); // 填充+描边 // 圆形/椭圆 doc.circle(60, 80, 20, 'FD'); // 圆形 doc.ellipse(120, 80, 30, 20, 'FD'); // 椭圆 // 多边形 const triangle = [[100, 120], [120, 100], [140, 120]]; doc.setFillColor(0, 255, 0); doc.poly(triangle, 'F'); // 路径 doc.setDrawColor(128, 0, 128); doc.setLineWidth(2); doc.path('M 160 100 L 180 120 L 160 140 Z'); // SVG路径语法 // 虚线 doc.setLineDashPattern([5, 5], 0); // 5像素实线,5像素间隔 doc.line(20, 150, 200, 150); doc.setLineDashPattern([], 0); // 恢复实线
2.1.6 表格生成
// 使用autoTable插件 import jsPDF from 'jspdf'; import 'jspdf-autotable'; const doc = new jsPDF(); // 简单表格 doc.autoTable({ head: [['ID', '姓名', '年龄', '城市']], body: [ ['1', '张三', '28', '北京'], ['2', '李四', '32', '上海'], ['3', '王五', '25', '广州'] ], startY: 20, theme: 'grid', // 主题:striped, grid, plain styles: { fontSize: 10, cellPadding: 3, overflow: 'linebreak' }, headStyles: { fillColor: [22, 160, 133], textColor: 255, fontStyle: 'bold' }, columnStyles: { 0: { cellWidth: 20 }, // 第一列宽度 1: { cellWidth: 40 } }, margin: { top: 20 }, didDrawPage: function(data) { // 每页绘制的回调 doc.setFontSize(10); doc.text(`第 ${data.pageNumber} 页`, data.settings.margin.left, doc.internal.pageSize.height - 10); } }); // 多页表格 const largeData = []; for (let i = 0; i < 100; i++) { largeData.push([i + 1, `用户${i}`, Math.floor(Math.random() * 50 + 18), '城市']); } doc.autoTable({ head: [['ID', '姓名', '年龄', '城市']], body: largeData, startY: 20, pageBreak: 'auto', // 自动分页 rowPageBreak: 'avoid', // 避免行内分页 showHead: 'everyPage' // 每页都显示表头 });

2.2 高级功能

2.2.1 页码和页眉页脚
function addHeaderFooter(doc, totalPages) { const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); // 页眉 doc.setFontSize(10); doc.setTextColor(100, 100, 100); doc.text('公司名称', 20, 15); doc.text(new Date().toLocaleDateString(), pageWidth - 20, 15, { align: 'right' }); // 页脚 doc.setFontSize(9); doc.text(`第 ${doc.internal.getCurrentPageInfo().pageNumber} 页 / 共 ${totalPages} 页`, pageWidth / 2, pageHeight - 10, { align: 'center' }); // 页脚线 doc.setDrawColor(200, 200, 200); doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15); } // 在每页绘制时调用 const totalPages = 5; for (let i = 1; i <= totalPages; i++) { if (i > 1) doc.addPage(); addHeaderFooter(doc, totalPages); // 添加内容... }
2.2.2 水印功能
function addWatermark(doc, text) { const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); // 保存当前状态 doc.saveGraphicsState(); // 设置水印样式 doc.setFontSize(40); doc.setTextColor(200, 200, 200, 0.3); // 半透明灰色 doc.setFont('helvetica', 'bold'); // 计算文本宽度 const textWidth = doc.getStringUnitWidth(text) * doc.internal.getFontSize() / doc.internal.scaleFactor; // 旋转和重复水印 const angle = -45 * Math.PI / 180; const spacing = 150; doc.saveGraphicsState(); for (let x = -pageWidth; x < pageWidth * 2; x += spacing) { for (let y = -pageHeight; y < pageHeight * 2; y += spacing) { doc.saveGraphicsState(); doc.translate(x, y); doc.rotate(angle, { origin: [0, 0] }); doc.text(text, 0, 0); doc.restoreGraphicsState(); } } doc.restoreGraphicsState(); } // 使用水印 const doc = new jsPDF(); doc.text('文档内容', 20, 20); addWatermark(doc, '机密文件');
2.2.3 链接和书签
// 内部链接 doc.textWithLink('点击跳转到第2页', 20, 20, { pageNumber: 2 }); // 外部链接 doc.textWithLink('访问官网', 20, 40, { url: 'https://example.com' }); // 邮件链接 doc.textWithLink('发送邮件', 20, 60, { url: 'mailto:[email protected]' }); // 添加书签 doc.outline.add(null, "封面", 1); doc.outline.add(null, "第一章", 2); doc.outline.add(1, "1.1 简介", 3); // 子书签 // 可点击目录 const tocY = 30; const chapters = [ { title: "第一章 简介", page: 1 }, { title: "第二章 基础", page: 3 }, { title: "第三章 高级", page: 5 } ]; chapters.forEach((chapter, i) => { doc.text(chapter.title, 20, tocY + i * 10); doc.textWithLink(`第${chapter.page}页`, 150, tocY + i * 10, { pageNumber: chapter.page }); });

第三部分:HTML2Canvas深度解析

3.1 核心原理与架构

HTML2Canvas的工作原理相当复杂,它本质上是一个简化的浏览器渲染引擎。让我们深入了解它的工作流程:

3.1.2 核心组件
// HTML2Canvas内部结构示意 class HTML2CanvasRenderer { constructor(element, options) { this.element = element; this.options = options; this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); } async render() { // 1. 解析DOM树 const root = this.parseDOM(this.element); // 2. 计算样式 this.calculateStyles(root); // 3. 创建渲染树 const renderTree = this.createRenderTree(root); // 4. 布局计算 this.calculateLayout(renderTree); // 5. 绘制 await this.draw(renderTree); return this.canvas; } // ... 其他方法 }

3.2 配置参数详解

const options = { // 基本配置 allowTaint: false, // 是否允许污染canvas useCORS: true, // 是否使用CORS加载图片 backgroundColor: '#ffffff', // 背景色 scale: 2, // 缩放比例(设备像素比) width: null, // 自定义宽度 height: null, // 自定义高度 // 日志和调试 logging: true, // 启用日志 onclone: null, // DOM克隆后的回调 // 图像配置 imageTimeout: 15000, // 图片加载超时(ms) proxy: null, // 代理服务器URL removeContainer: true, // 是否移除临时容器 // 高级配置 foreignObjectRendering: false, // 使用foreignObject(SVG) ignoreElements: (element) => false, // 忽略特定元素 onrendered: null, // 渲染完成回调 // 性能配置 async: true, // 异步渲染 cacheBust: false, // 缓存破坏 letterRendering: false, // 文字渲染优化 // Canvas配置 canvas: null, // 使用现有canvas x: 0, // 水平偏移 y: 0, // 垂直偏移 scrollX: 0, // 水平滚动 scrollY: 0, // 垂直滚动 windowWidth: window.innerWidth, // 窗口宽度 windowHeight: window.innerHeight // 窗口高度 };

第四部分:JSPDF与HTML2Canvas集成实战

4.1 基础集成方案

// 基础集成函数 async function exportToPDF(elementId, fileName = 'document.pdf', options = {}) { // 默认配置 const defaultOptions = { pdf: { orientation: 'p', unit: 'mm', format: 'a4', compress: true }, html2canvas: { scale: 2, useCORS: true, logging: false }, margin: { top: 15, right: 15, bottom: 15, left: 15 } }; // 合并配置 const config = { pdf: { ...defaultOptions.pdf, ...options.pdf }, html2canvas: { ...defaultOptions.html2canvas, ...options.html2canvas }, margin: { ...defaultOptions.margin, ...options.margin } }; try { // 1. 获取目标元素 const element = document.getElementById(elementId); if (!element) { throw new Error(`元素 #${elementId} 未找到`); } // 2. 使用html2canvas生成canvas console.log('开始生成canvas...'); const canvas = await html2canvas(element, config.html2canvas); // 3. 获取图片数据 const imgData = canvas.toDataURL('image/png', 1.0); const imgProps = { width: canvas.width, height: canvas.height }; // 4. 计算PDF尺寸 const pdfWidth = config.pdf.format === 'a4' ? 210 : 297; // A4宽度210mm const pdfHeight = config.pdf.format === 'a4' ? 297 : 210; // A4高度297mm // 可用宽度 = PDF宽度 - 左右边距 const usableWidth = pdfWidth - config.margin.left - config.margin.right; // 5. 计算缩放比例 const pixelsPerMM = imgProps.width / usableWidth; const scale = usableWidth / imgProps.width; const imgHeightMM = imgProps.height * scale; // 6. 创建PDF console.log('创建PDF文档...'); const doc = new jsPDF(config.pdf); // 7. 处理分页 const pageHeight = pdfHeight - config.margin.top - config.margin.bottom; let position = 0; if (imgHeightMM <= pageHeight) { // 一页能放下 doc.addImage(imgData, 'PNG', config.margin.left, config.margin.top, usableWidth, imgHeightMM); } else { // 需要分页 while (position < imgHeightMM) { if (position > 0) { doc.addPage(); } doc.addImage(imgData, 'PNG', config.margin.left, config.margin.top - position, usableWidth, imgHeightMM); position += pageHeight; } } // 8. 保存PDF doc.save(fileName); console.log('PDF导出完成'); return doc; } catch (error) { console.error('导出PDF失败:', error); throw error; } } // 使用示例 document.getElementById('export-btn').addEventListener('click', async () => { try { await exportToPDF('content', '报告.pdf', { pdf: { format: 'a4', orientation: 'portrait' }, html2canvas: { scale: 3, // 更高清 backgroundColor: '#ffffff' }, margin: { top: 20, right: 20, bottom: 20, left: 20 } }); } catch (error) { alert('导出失败: ' + error.message); } });

4.2 高级集成:智能分页与表格保护

class SmartPDFExporter { constructor(options = {}) { this.options = this.mergeOptions(options); this.pageBreaks = []; } mergeOptions(options) { const defaults = { elementId: null, fileName: 'export.pdf', pdf: { orientation: 'p', unit: 'mm', format: 'a4' }, html2canvas: { scale: 2, logging: false, useCORS: true }, styling: { tableProtection: true, keepTogetherClass: 'keep-together', avoidBreakClass: 'avoid-break' }, margins: { top: 15, right: 15, bottom: 15, left: 15 }, features: { pageNumbers: true, header: true, footer: true, watermark: false } }; return this.deepMerge(defaults, options); } deepMerge(target, source) { for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; this.deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; } async export() { try { // 1. 验证和准备 this.validateInput(); // 2. 应用保护样式 if (this.options.styling.tableProtection) { this.applyProtectionStyles(); } // 3. 分析DOM结构 const analysis = await this.analyzeDOM(); // 4. 计算分页点 this.calculatePageBreaks(analysis); // 5. 生成canvas const canvas = await this.generateCanvas(); // 6. 创建PDF const pdf = await this.generatePDF(canvas, analysis); // 7. 保存 pdf.save(this.options.fileName); return { success: true, pdf }; } catch (error) { console.error('导出失败:', error); throw error; } } validateInput() { if (!this.options.elementId) { throw new Error('elementId 必须提供'); } const element = document.getElementById(this.options.elementId); if (!element) { throw new Error(`元素 #${this.options.elementId} 未找到`); } this.element = element; } applyProtectionStyles() { const style = document.createElement('style'); style.textContent = ` .${this.options.styling.keepTogetherClass} { page-break-inside: avoid !important; break-inside: avoid !important; } .${this.options.styling.avoidBreakClass} { page-break-before: avoid !important; break-before: avoid !important; } table { page-break-inside: avoid !important; break-inside: avoid !important; border-collapse: collapse !important; } tr { page-break-inside: avoid !important; break-inside: avoid !important; } `; // 移除可能存在的旧样式 const oldStyles = this.element.querySelectorAll('style[data-pdf-protection]'); oldStyles.forEach(s => s.remove()); style.setAttribute('data-pdf-protection', 'true'); this.element.appendChild(style); // 为表格添加保护类 const tables = this.element.querySelectorAll('table'); tables.forEach(table => { table.classList.add(this.options.styling.keepTogetherClass); }); // 为重要元素添加避免分页类 const importantElements = this.element.querySelectorAll('h1, h2, .important'); importantElements.forEach(el => { el.classList.add(this.options.styling.avoidBreakClass); }); } async analyzeDOM() { const tables = this.element.querySelectorAll('table'); const importantElements = this.element.querySelectorAll(`.${this.options.styling.keepTogetherClass}`); const elementRect = this.element.getBoundingClientRect(); const analysis = { tables: [], importantElements: [], containerHeight: this.element.offsetHeight, containerWidth: this.element.offsetWidth }; // 分析表格 tables.forEach((table, index) => { const rect = table.getBoundingClientRect(); analysis.tables.push({ index, element: table, top: rect.top - elementRect.top, bottom: rect.bottom - elementRect.top, height: rect.height, rowCount: table.querySelectorAll('tr').length, canBreak: !table.classList.contains(this.options.styling.keepTogetherClass) }); }); // 分析重要元素 importantElements.forEach((el, index) => { if (el.tagName !== 'TABLE') { // 表格已经分析过了 const rect = el.getBoundingClientRect(); analysis.importantElements.push({ index, element: el, top: rect.top - elementRect.top, bottom: rect.bottom - elementRect.top, height: rect.height, tagName: el.tagName }); } }); // 按位置排序 analysis.tables.sort((a, b) => a.top - b.top); analysis.importantElements.sort((a, b) => a.top - b.top); return analysis; } calculatePageBreaks(analysis) { const pdf = new jsPDF(this.options.pdf); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const usableWidth = pdfWidth - this.options.margins.left - this.options.margins.right; const usableHeight = pdfHeight - this.options.margins.top - this.options.margins.bottom; // 像素到毫米的转换比例 const pxToMM = usableWidth / analysis.containerWidth; const breaks = []; let currentPageHeightMM = 0; // 处理所有需要保护的元素 const allElements = [...analysis.tables, ...analysis.importantElements] .sort((a, b) => a.top - b.top); allElements.forEach((element, index) => { const elementHeightMM = element.height * pxToMM; // 检查是否放得下 if (currentPageHeightMM + elementHeightMM > usableHeight && currentPageHeightMM > 0) { // 放不下,需要分页 if (index > 0) { const prevElement = allElements[index - 1]; // 在元素前分页 breaks.push({ type: 'before', element: element, position: element.top, reason: `${element.tagName || 'table'} 无法放入当前页` }); currentPageHeightMM = elementHeightMM; } } else { // 放得下 currentPageHeightMM += elementHeightMM; } }); this.pageBreaks = breaks; console.log('计算出的分页点:', this.pageBreaks); } async generateCanvas() { // 固定尺寸,防止渲染时变化 const originalStyle = { width: this.element.style.width, height: this.element.style.height, position: this.element.style.position }; this.element.style.width = `${this.element.offsetWidth}px`; this.element.style.height = `${this.element.offsetHeight}px`; this.element.style.position = 'relative'; try { const canvas = await html2canvas(this.element, this.options.html2canvas); return canvas; } finally { // 恢复原始样式 Object.assign(this.element.style, originalStyle); } } async generatePDF(canvas, analysis) { const pdf = new jsPDF(this.options.pdf); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const usableWidth = pdfWidth - this.options.margins.left - this.options.margins.right; const usableHeight = pdfHeight - this.options.margins.top - this.options.margins.bottom; // 计算缩放 const scale = usableWidth / canvas.width; const totalHeightMM = canvas.height * scale; // 如果有分页点,使用智能分页 if (this.pageBreaks.length > 0) { await this.generatePDFWithBreaks(pdf, canvas, scale, usableWidth, usableHeight); } else { // 无分页点,使用简单分页 await this.generatePDFSimple(pdf, canvas, scale, usableWidth, usableHeight); } // 添加页眉页脚 if (this.options.features.header || this.options.features.footer) { this.addHeaderFooter(pdf, this.pageBreaks.length + 1); } // 添加水印 if (this.options.features.watermark) { this.addWatermark(pdf); } return pdf; } async generatePDFWithBreaks(pdf, canvas, scale, usableWidth, usableHeight) { const imgData = canvas.toDataURL('image/png', 1.0); // 转换分页点为canvas像素位置 const breakPoints = this.pageBreaks.map(breakInfo => breakInfo.position); breakPoints.sort((a, b) => a - b); // 确保从0开始,到canvas高度结束 const allPoints = [0, ...breakPoints, canvas.height]; let startY = 0; for (let i = 1; i < allPoints.length; i++) { const segmentHeight = allPoints[i] - startY; if (segmentHeight <= 0) continue; // 创建当前页的canvas片段 const segmentCanvas = document.createElement('canvas'); segmentCanvas.width = canvas.width; segmentCanvas.height = segmentHeight; const ctx = segmentCanvas.getContext('2d'); ctx.drawImage( canvas, 0, startY, canvas.width, segmentHeight, 0, 0, canvas.width, segmentHeight ); const segmentImgData = segmentCanvas.toDataURL('image/png', 1.0); const segmentHeightMM = segmentHeight * scale; // 如果不是第一页,添加新页 if (i > 1) { pdf.addPage(); } // 添加图片到PDF pdf.addImage( segmentImgData, 'PNG', this.options.margins.left, this.options.margins.top, usableWidth, segmentHeightMM ); startY = allPoints[i]; } } generatePDFSimple(pdf, canvas, scale, usableWidth, usableHeight) { const imgData = canvas.toDataURL('image/png', 1.0); const totalHeightMM = canvas.height * scale; if (totalHeightMM <= usableHeight) { // 一页能放下 pdf.addImage( imgData, 'PNG', this.options.margins.left, this.options.margins.top, usableWidth, totalHeightMM ); } else { // 需要分页 let position = 0; while (position < totalHeightMM) { if (position > 0) { pdf.addPage(); } pdf.addImage( imgData, 'PNG', this.options.margins.left, this.options.margins.top - position, usableWidth, totalHeightMM ); position += usableHeight; } } } addHeaderFooter(pdf, totalPages) { const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const currentPage = pdf.internal.getCurrentPageInfo().pageNumber; // 保存当前状态 const originalFont = pdf.internal.getFont(); const originalSize = pdf.internal.getFontSize(); const originalColor = pdf.internal.getTextColor(); // 页眉 if (this.options.features.header) { pdf.setFontSize(10); pdf.setTextColor(100, 100, 100); // 左侧:标题 pdf.text( this.options.fileName.replace('.pdf', ''), this.options.margins.left, this.options.margins.top - 10 ); // 右侧:日期 const dateStr = new Date().toLocaleDateString(); pdf.text( dateStr, pdfWidth - this.options.margins.right, this.options.margins.top - 10, { align: 'right' } ); // 页眉线 pdf.setDrawColor(200, 200, 200); pdf.line( this.options.margins.left, this.options.margins.top - 5, pdfWidth - this.options.margins.right, this.options.margins.top - 5 ); } // 页脚 if (this.options.features.footer) { // 页脚线 pdf.setDrawColor(200, 200, 200); pdf.line( this.options.margins.left, pdfHeight - this.options.margins.bottom + 5, pdfWidth - this.options.margins.right, pdfHeight - this.options.margins.bottom + 5 ); // 页码 if (this.options.features.pageNumbers) { pdf.setFontSize(9); pdf.setTextColor(100, 100, 100); const pageText = `第 ${currentPage} 页 / 共 ${totalPages} 页`; pdf.text( pageText, pdfWidth / 2, pdfHeight - this.options.margins.bottom + 10, { align: 'center' } ); } } // 恢复原始状态 pdf.setFont(originalFont[0], originalFont[1]); pdf.setFontSize(originalSize); pdf.setTextColor(originalColor[0], originalColor[1], originalColor[2]); } addWatermark(pdf) { const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); // 保存状态 pdf.saveGraphicsState(); // 设置水印样式 pdf.setFontSize(40); pdf.setTextColor(200, 200, 200, 0.1); pdf.setFont('helvetica', 'bold'); // 旋转和重复 const angle = -45 * Math.PI / 180; const text = this.options.features.watermark.text || 'CONFIDENTIAL'; for (let x = -pdfWidth; x < pdfWidth * 2; x += 150) { for (let y = -pdfHeight; y < pdfHeight * 2; y += 150) { pdf.saveGraphicsState(); pdf.translate(x, y); pdf.rotate(angle, { origin: [0, 0] }); pdf.text(text, 0, 0); pdf.restoreGraphicsState(); } } pdf.restoreGraphicsState(); } } // 使用示例 const exporter = new SmartPDFExporter({ elementId: 'report-content', fileName: '智能报告.pdf', pdf: { format: 'a4', orientation: 'portrait' }, html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff' }, features: { pageNumbers: true, header: true, footer: true, watermark: { text: '公司机密', enabled: true } }, styling: { tableProtection: true, keepTogetherClass: 'pdf-keep-together' } }); // 触发导出 document.getElementById('smart-export').addEventListener('click', async () => { try { await exporter.export(); alert('导出成功!'); } catch (error) { alert('导出失败: ' + error.message); } });

4.3 响应式与移动端适配

class ResponsivePDFExporter { constructor(options = {}) { this.options = { breakpoints: { mobile: 768, tablet: 1024, desktop: 1200 }, scaling: { mobile: 1, tablet: 1.5, desktop: 2 }, ...options }; } detectDeviceType() { const width = window.innerWidth; if (width < this.options.breakpoints.mobile) { return 'mobile'; } else if (width < this.options.breakpoints.tablet) { return 'tablet'; } else { return 'desktop'; } } async exportResponsive(elementId, fileName) { const deviceType = this.detectDeviceType(); const scale = this.options.scaling[deviceType] || 1.5; console.log(`设备类型: ${deviceType}, 使用缩放: ${scale}`); // 调整元素样式以适应PDF const element = document.getElementById(elementId); const originalStyles = this.backupStyles(element); try { // 应用响应式样式 this.applyResponsiveStyles(element, deviceType); // 生成PDF const canvas = await html2canvas(element, { scale, useCORS: true, backgroundColor: '#ffffff' }); const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const imgWidth = canvas.width; const imgHeight = canvas.height; // 计算缩放比例 const margin = 10; const usableWidth = pdfWidth - 2 * margin; const scaleFactor = usableWidth / imgWidth; pdf.addImage( canvas.toDataURL('image/png', 1.0), 'PNG', margin, margin, usableWidth, imgHeight * scaleFactor ); pdf.save(fileName); } finally { // 恢复原始样式 this.restoreStyles(element, originalStyles); } } backupStyles(element) { return { width: element.style.width, height: element.style.height, fontSize: element.style.fontSize, padding: element.style.padding, margin: element.style.margin, display: element.style.display }; } applyResponsiveStyles(element, deviceType) { // 根据设备类型应用不同的样式 const styles = { mobile: { width: '100%', fontSize: '12px', padding: '10px', margin: '0', display: 'block' }, tablet: { width: '90%', fontSize: '13px', padding: '15px', margin: '0 auto', display: 'block' }, desktop: { width: '80%', fontSize: '14px', padding: '20px', margin: '0 auto', display: 'block' } }; const deviceStyles = styles[deviceType] || styles.desktop; Object.assign(element.style, deviceStyles); // 特别处理表格 const tables = element.querySelectorAll('table'); tables.forEach(table => { if (deviceType === 'mobile') { table.style.fontSize = '10px'; table.style.width = '100%'; } }); } restoreStyles(element, originalStyles) { Object.assign(element.style, originalStyles); } } // 使用响应式导出 const responsiveExporter = new ResponsivePDFExporter(); // 响应式导出按钮 document.getElementById('responsive-export').addEventListener('click', () => { responsiveExporter.exportResponsive('content', '响应式报告.pdf'); });

Read more

跟着AI学Java,三天零基础入门到大牛,基础学习到SpringBoot项目实战一套通关,基于DeepSeek大模型通义灵码,mysql数据库,小程序vue3前端

跟着AI学Java,三天零基础入门到大牛,基础学习到SpringBoot项目实战一套通关,基于DeepSeek大模型通义灵码,mysql数据库,小程序vue3前端

关于什么是java我就不在啰嗦,大家如果不知道可以自行问ai 开发者工具 传统模式下我们学习Java需要用到IntelliJ IDEA或者Eclipse,但是现在是ai人工智能时代,我们可以借助ai快速学习,甚至可以借助ai快速的实现不写一行代码,就可以实现一个Java项目,所以ai人工智能时代我们要选择一款得心应手的Java开发者工具。我这里推荐使用 以下是市面上主流的 Java 开发工具及其优缺点分析: 1. IntelliJ IDEA * 使用场景:企业级开发,适合复杂项目。 * 优点: * 强大的代码补全和重构功能。 * 内置对 Spring、Maven、Gradle 等框架的良好支持。 * 高效的调试工具和性能分析器。 * 插件生态系统丰富。 * 缺点: * 商业版收费(社区版功能有限)。 * 占用内存较大,启动较慢。 2. Eclipse * 使用场景:广泛应用于企业级和开源项目。 * 优点: * 免费开源,插件丰富。 * 轻量级配置(基础版本占用资源较少)。 * 对 Java EE 和 An

【保姆级教程】无成本零门槛安装配置OpenClaw龙虾AI全能助手

【保姆级教程】无成本零门槛安装配置OpenClaw龙虾AI全能助手

哈喽大家好!最近爆火的 OpenClaw(龙虾AI)全能助手大家体验了吗?它不仅能帮你自动整理邮件、查询天气,还能全自动写小红书笔记并发布,简直是打工人和自媒体人的摸鱼神器! 很多小伙伴想玩但又怕配置太复杂、花销太大。今天给大家带来一篇零门槛、保姆级的安装配置教程!教你如何低成本获取云服务器,轻松实现 AI 大模型自由。全程图文指引,小白也能轻松搞定,赶紧跟着操作起来吧! 一、获取云服务器 想要畅玩 OpenClaw,首先我们需要一个服务器。这次教大家如何获取腾讯云轻量服务器来进行配置。 ⏰ 活动时间:2026年1月21日 - 3月31日 腾讯推出了登录 CodeBuddy 送 2C2G4M 轻量服务器的限时活动:登录先送1个月,活跃7天再送2个月。 👉 【官方地址】:https://www.codebuddy.cn/promotion/?ref=ie2rwhd1loq 根据页面提示安装好软件并登录账号后,直接选择一个月的轻量应用服务器即可。 之后只要累计活跃7天就能续费两个月(每天和 AI

AI赋能专利翻译,八月瓜科技“妙算翻译大模型”亮相国际论坛

AI赋能专利翻译,八月瓜科技“妙算翻译大模型”亮相国际论坛

当前,国家高度重视人工智能与知识产权融合发展,《新一代人工智能发展规划》明确提出“推动人工智能在知识产权检索、分析、翻译等领域的深度应用,提升知识产权服务效率与质量”,《“十四五”国家知识产权保护和运用规划》也强调“加强知识产权信息化、智能化基础设施建设,推动专利信息跨语言互通”。 顺应这一政策导向,专利领域对专业化翻译的需求愈发迫切。八月瓜科技“妙算翻译大模型”立足需求,凭借深厚的技术积累与精准的场景适配,成为破解行业痛点、助力跨境创新的核心力量。 国际论坛亮相获认可,产品实力彰显初心 日前,妙算翻译大模型凭借在专利翻译领域的突出实力与创新成果,亮相东盟+中日韩(10+3)人工智能产业发展论坛,成为论坛上聚焦知识产权服务智能化的亮点成果,获得了行业专家、参会企业及相关机构的高度关注与广泛认可。此次论坛亮相,不仅是对妙算翻译大模型技术实力与应用价值的权威肯定,更彰显了其在推动专利翻译智能化、打破跨国创新语言壁垒方面的重要作用,为其进一步拓展市场、服务更多科技创新主体奠定了坚实基础。 能获得行业广泛认可,核心源于产品本身的专业定位与硬核实力。妙算翻译大模型在语言

字节开源 DeerFlow 2.0——登顶 GitHub Trending 1,让 AI 可做任何事情

字节开源 DeerFlow 2.0——登顶 GitHub Trending 1,让 AI 可做任何事情

打开 deerflow 的官网,瞬间被首页的这段文字震撼到了,do anything with deerflow。让 agent 做任何事情,这让我同时想到了 openclaw 刚上线时场景。 字节跳动将 DeerFlow 彻底重写,发布 2.0 版本,并在发布当天登上 GitHub Trending 第一名。这不是一次功能迭代,而是一次从"深度研究框架"到"Super Agent 运行时基础设施"的彻底蜕变。 背景:从 v1 到 v2,发生了什么? DeerFlow(Deep Exploration and Efficient Research Flow)