@neosjs/html-to-pdf
Version:
将html转换为pdf,支持中文,支持图片,支持表格,支持分页,支持水印,支持自定义页眉页脚,支持页码. 解决分页截断问题.
9 lines • 17.6 kB
JavaScript
/*!
* @neosjs/html-to-pdf
* Version: 1.2.0
* Copyright (c) 2021-PRESENT NeosJS
* ReleaseTime: 2025/7/29 17:02:31
*/
import e from"jspdf";const t=`FZLTXIHJW`,n=6,r={elementId:``,wholeNodeClass:``,ignoreElementsClass:``,orientation:`p`,format:`a4`,intervalHeight:5,open:!1,enablePerformanceMonitor:!1,pageMargin:{top:10,bottom:10,left:10,right:10}},i={a0:{width:841,height:1189},a1:{width:594,height:841},a2:{width:420,height:594},a3:{width:297,height:420},a4:{width:210,height:297},a5:{width:148,height:210},a6:{width:105,height:148},a7:{width:74,height:105},a8:{width:52,height:74},letter:{width:215.9,height:279.4},legal:{width:215.9,height:355.6},tabloid:{width:279.4,height:431.8},ledger:{width:431.8,height:279.4}},a={MIN_PAGE_HEIGHT:50,MAX_CANVAS_SIZE:16384,MAX_MEMORY_USAGE:512,MAX_PDF_SIZE:100},o={DEFAULT_FONT_FAMILY:`Arial, sans-serif`,DEFAULT_FONT_SIZE:`14px`,DEFAULT_LINE_HEIGHT:`1.5`,DEFAULT_BACKGROUND_COLOR:`#ffffff`,DEFAULT_TEXT_COLOR:`#000000`,DEFAULT_HEADER_FONT_SIZE:10,DEFAULT_HEADER_COLOR:`#ddd`},s={ELEMENT_NOT_FOUND:`ELEMENT_NOT_FOUND`,EMPTY_ELEMENT:`EMPTY_ELEMENT`,CANVAS_GENERATION_FAILED:`CANVAS_GENERATION_FAILED`,PDF_CREATION_FAILED:`PDF_CREATION_FAILED`,MEMORY_ERROR:`MEMORY_ERROR`,INVALID_OPTIONS:`INVALID_OPTIONS`},c={SLOW_OPERATION:5e3,MEMORY_WARNING:100,CANVAS_SIZE_WARNING:8192};var l=class{static createError(e,t,n){let r=Error(t);return r.code=s[e],r.details=n,r}static validateElement(e,t){if(!e)throw this.createError(`ELEMENT_NOT_FOUND`,`未找到目标DOM元素: ${t}`);if(e.children.length===0)throw this.createError(`EMPTY_ELEMENT`,`目标元素没有子节点,无法生成PDF`);if(e.offsetWidth===0||e.offsetHeight===0)throw this.createError(`EMPTY_ELEMENT`,`元素尺寸为0,无法转换`)}static validateCanvas(e){let t=e.getContext(`2d`);if(!t)throw this.createError(`CANVAS_GENERATION_FAILED`,`无法获取Canvas上下文`);let n=t.getImageData(0,0,e.width,e.height),r=n.data.some(e=>e!==0);if(!r)throw this.createError(`CANVAS_GENERATION_FAILED`,`生成的canvas没有内容`)}static checkMemoryUsage(){if(`memory`in performance){let e=performance.memory,t=e.usedJSHeapSize/1024/1024;t>512&&console.warn(`⚠️ 内存使用过高: ${t.toFixed(2)}MB`)}}static checkCanvasSize(e,t){if(e>16384||t>16384)throw this.createError(`CANVAS_GENERATION_FAILED`,`Canvas尺寸过大,可能导致内存问题`)}static async handleAsyncError(e,t){try{return await e}catch(e){throw console.error(`❌ ${t}:`,e),e}}},u=class{static getElement(e){return typeof e==`string`?document.getElementById(e):e}static createTempContainer(e){let t=document.createElement(`div`);return t.style.position=`absolute`,t.style.left=`-99999px`,t.style.top=`0`,t.style.width=`${e}px`,t.style.backgroundColor=o.DEFAULT_BACKGROUND_COLOR,t.style.overflow=`visible`,t.style.fontFamily=o.DEFAULT_FONT_FAMILY,t.style.fontSize=o.DEFAULT_FONT_SIZE,t.style.lineHeight=o.DEFAULT_LINE_HEIGHT,t.style.color=o.DEFAULT_TEXT_COLOR,document.body.appendChild(t),t}static cleanupTempContainer(e){e&&e.parentNode&&e.parentNode.removeChild(e)}static cleanupTempElements(e){let t=e.querySelectorAll(`.empty-node`);t.forEach(e=>{e.parentNode&&e.parentNode.removeChild(e)})}static setElementStyles(e){e.style.backgroundColor=o.DEFAULT_BACKGROUND_COLOR,e.style.color=o.DEFAULT_TEXT_COLOR,e.style.fontFamily=o.DEFAULT_FONT_FAMILY,e.style.fontSize=o.DEFAULT_FONT_SIZE,e.style.lineHeight=o.DEFAULT_LINE_HEIGHT}static processNonBreakableElements(e){let{tempContainer:t,pageHeight:n,wholeNodeClass:r,intervalHeight:i=5}=e,a=t.querySelectorAll(`.${r}`),o=0;return a.forEach(e=>{let t=e,r=t.offsetTop,a=t.offsetHeight,s=r+a,c=Math.floor(r/n)*n,l=c+n;if(s>l&&r<l){let e=l-r+i,n=this.createEmptyBlock(e);t.parentNode?.insertBefore(n,t),o+=e}}),o}static createEmptyBlock(e){let t=document.createElement(`div`);return t.className=`empty-node`,t.style.background=o.DEFAULT_BACKGROUND_COLOR,t.style.height=`${e}px`,t.style.position=`relative`,t.style.zIndex=`-1`,t}static cloneElement(e){return e.cloneNode(!0)}static shouldIgnoreElement(e,t){return String(e?.className)?.indexOf(t)!==-1}};const d=e=>{if(!e||typeof e!=`string`)return[0,0,0];if(e.startsWith(`#`))return f(e);let t=p(e);return t||[0,0,0]};function f(e){let t=e.slice(1);if(t.length===3&&(t=t.split(``).map(e=>e+e).join(``)),t.length===6){let e=Number.parseInt(t.slice(0,2),16),n=Number.parseInt(t.slice(2,4),16),r=Number.parseInt(t.slice(4,6),16);if(!Number.isNaN(e)&&!Number.isNaN(n)&&!Number.isNaN(r))return[e,n,r]}return[0,0,0]}function p(e){let t=e.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);if(t){let e=Number.parseInt(t[1]),n=Number.parseInt(t[2]),r=Number.parseInt(t[3]);if(!Number.isNaN(e)&&!Number.isNaN(n)&&!Number.isNaN(r))return[e,n,r]}return null}const m={content:``,size:40,opacity:.2,angle:45,color:`#969696`};function h(e){let t={...m,...e};return typeof t.content!=`string`&&(console.warn(`水印内容必须是字符串类型`),t.content=m.content),(typeof t.size!=`number`||t.size<=0)&&(console.warn(`水印字体大小必须是正数`),t.size=m.size),(typeof t.opacity!=`number`||t.opacity<0||t.opacity>1)&&(console.warn(`水印透明度必须在0-1之间`),t.opacity=m.opacity),typeof t.angle!=`number`&&(console.warn(`水印角度必须是数字类型`),t.angle=m.angle),t}function g(e,t,n){let r=[],i=20;for(let a=0;a<n;a++){let o=a%2==0,s=o?e*.1+i:e*.6+i,c=t*(a+1)/(n+1)+i;r.push({x:s,y:c})}return r}function _(e,n){try{e.saveGraphicsState();let r=new e.GState({opacity:n.opacity});e.setGState(r),e.setFont(t,`normal`),e.setFontSize(n.size);let i=d(n.color);e.setTextColor(i[0],i[1],i[2])}catch(t){console.warn(`设置水印样式失败:`,t),e.setFontSize(n.size);let r=d(n.color);e.setTextColor(r[0],r[1],r[2])}}function v(e,t,n,r,i){try{e.text(t,n,r,{angle:i})}catch(e){console.warn(`绘制水印文本失败:`,e)}}function y(e){e.API.addWatermark=function(e={}){let t=this;try{let r=h(e);if(!r.content.trim())return;let i=t.internal.pageSize.getWidth(),a=t.internal.pageSize.getHeight(),o=g(i,a,n);_(t,r),o.forEach(({x:e,y:n})=>{v(t,r.content,e,n,r.angle)}),t.restoreGraphicsState()}catch(e){console.error(`添加水印失败:`,e),this.handleWatermarkError(t)}},e.API.addWatermarkToAllPages=function(e={}){let t=this;try{let n=t.internal.getNumberOfPages();for(let r=1;r<=n;r++)t.setPage(r),t.addWatermark(e)}catch(e){console.error(`为所有页面添加水印失败:`,e)}},e.API.handleWatermarkError=function(e){try{e.restoreGraphicsState()}catch(e){console.warn(`恢复图形状态失败:`,e)}}}const b=`#000000`,x=10,S=50,C=[221,221,221],w=`image/jpeg`,T=`JPEG`;var E=class extends Error{constructor(e,t){super(e),this.code=t,this.name=`PdfUtilsError`}},D=class{static setTextStyle(e,n=x,r=b){try{e.setFont(t,`normal`),e.setFontSize(n);let i=d(r);e.setTextColor(i[0],i[1],i[2])}catch(t){console.warn(`设置文本样式失败:`,t),e.setFontSize(n)}}static addHeaderFooter(e,t,n,r,i,a){try{i&&this.addHeader(e,i,n,r),a&&this.addFooter(e,a,t,n,r)}catch(e){console.warn(`添加页眉页脚失败:`,e)}}static addHeader(e,t,n,r){let{fontSize:i=x,color:a=b,content:o,align:s,border:c}=t;if(!o)return;let l=this.getAlignX(s,r.width);this.setTextStyle(e,i,a);try{e.text(o,l,n.top-2,{align:s})}catch(e){console.warn(`添加页眉文本失败:`,e)}c&&this.drawHeaderBorder(e,n,r)}static addFooter(e,t,n,r,i){let{fontSize:a=x,color:o=b,content:s,align:c,border:l}=t;if(!s)return;let u=this.replacePagePlaceholders(s,n),d=this.getAlignX(c,i.width);this.setTextStyle(e,a,o);try{e.text(u,d,i.height-r.bottom-3.5,{align:c})}catch(e){console.warn(`添加页脚文本失败:`,e)}l&&this.drawFooterBorder(e,r,i)}static replacePagePlaceholders(e,t){return e.replace(/\{page\}/g,t.currentPage.toString()).replace(/\{total\}/g,t.totalPages.toString())}static drawHeaderBorder(e,t,n){try{e.setDrawColor(C[0],C[1],C[2]),e.line(t.left,t.top+2.5,n.width-t.right,t.top+2.5),e.line(t.left,t.top+2,n.width-t.right,t.top+2)}catch(e){console.warn(`绘制页眉边框失败:`,e)}}static drawFooterBorder(e,t,n){try{e.setDrawColor(C[0],C[1],C[2]),e.line(t.left,n.height-t.bottom-10,n.width-t.right,n.height-t.bottom-10)}catch(e){console.warn(`绘制页脚边框失败:`,e)}}static getAlignX(e,t){switch(e){case`left`:return 10;case`right`:return t-10;default:return t/2}}static async createPdfDocument(t){return await import(`./fonts-0BPTFFwY.js`),new Promise((n,r)=>{try{this.validateConfig(t),y(e);let{canvas:r,contentWidth:i,contentHeight:a,margins:o,pdf:s,headerFooter:c,file:l,watermark:u={}}=t,{orientation:d,format:f,quality:p}=s,m=new e(d,`mm`,f,!0),{totalPages:h}=this.calculatePages(r,i,a);h===1?this.addSinglePage(m,r,i,a,o,c||{},p):this.addMultiplePages(m,r,i,a,o,c||{},p,h),m.addWatermarkToAllPages(u);let g=m.output(`blob`);this.handleFileOperations(m,g,l),n({blob:g,totalPages:h})}catch(e){console.error(`创建PDF文档失败:`,e),r(new E(`创建PDF文档失败`,`PDF_CREATION_FAILED`))}})}static validateConfig(e){if(!e.canvas)throw new E(`Canvas元素不能为空`,`INVALID_CANVAS`);if(!e.contentWidth||!e.contentHeight)throw new E(`内容尺寸不能为空`,`INVALID_DIMENSIONS`);if(!e.pdf)throw new E(`PDF配置不能为空`,`INVALID_PDF_CONFIG`)}static handleFileOperations(e,t,n){try{if(n?.fileName&&e.save(`${n.fileName}.pdf`),n?.open){let e=URL.createObjectURL(t);window.open(e,`_blank`)}}catch(e){console.warn(`文件操作失败:`,e)}}static calculatePages(e,t,n){let r=e.width,i=e.height,a=r/t*n,o=Math.max(a,S),s=Math.ceil(i/o);return{totalPages:s,currentPage:1}}static addSinglePage(e,t,n,r,i,a,o){let{top:s,left:c,bottom:l}=i,u=n,d=n/t.width*t.height;try{let n=t.toDataURL(w,o);e.addImage(n,T,c,s+4,u,d,void 0,`FAST`)}catch(e){throw console.warn(`添加图片到PDF失败:`,e),new E(`添加图片到PDF失败`,`IMAGE_ADDITION_FAILED`)}this.addHeaderFooter(e,{currentPage:1,totalPages:1,position:s+4,htmlHeight:t.height},i,{width:n+c*2,height:r+s+l},a?.header,a?.footer)}static addMultiplePages(e,t,n,r,i,a,o,s){let{top:c,left:l,bottom:u}=i,d=t.width,f=t.height,p=d/n*r,m=Math.max(p,S),h=f,g=0,_=1;for(;h>0;){let p=Math.min(m,h),v=n,y=p/f*(n/d*f);this.addPageContent(e,t,d,p,g,l,c,v,y,o),this.addHeaderFooter(e,{currentPage:_,totalPages:s,position:c+4,htmlHeight:h},i,{width:n+l*2,height:r+c+u},a?.header,a?.footer),h-=p,g+=p,_++,h>0&&e.addPage()}}static addPageContent(e,t,n,r,i,a,o,s,c,l){let u=document.createElement(`canvas`),d=u.getContext(`2d`);if(!d)throw new E(`无法创建临时Canvas上下文`,`CANVAS_CONTEXT_FAILED`);try{u.width=n,u.height=r,d.drawImage(t,0,i,n,r,0,0,n,r);let f=u.toDataURL(w,l);e.addImage(f,T,a,o+4,s,c,void 0,`FAST`)}catch(e){throw console.warn(`添加页面内容失败:`,e),new E(`添加页面内容失败`,`PAGE_CONTENT_FAILED`)}}static getPaperSize(e){let t={a0:{width:841,height:1189},a1:{width:594,height:841},a2:{width:420,height:594},a3:{width:297,height:420},a4:{width:210,height:297},a5:{width:148,height:210},a6:{width:105,height:148},a7:{width:74,height:105},a8:{width:52,height:74},letter:{width:215.9,height:279.4},legal:{width:215.9,height:355.6},tabloid:{width:279.4,height:431.8},ledger:{width:431.8,height:279.4}};return t[e]||t.a4}},O=class{static async convertWithHtml2Canvas(e,t={}){try{let n=await import(`html2canvas`),r=await n.default(e,{scale:t.scale||1.5,useCORS:t.useCORS??!0,allowTaint:t.allowTaint??!0,backgroundColor:t.backgroundColor||`#ffffff`,logging:t.logging??!1,width:e.offsetWidth,height:e.offsetHeight,scrollX:0,scrollY:0,windowWidth:e.offsetWidth,windowHeight:e.offsetHeight,removeContainer:!0,foreignObjectRendering:!1,onclone:t=>{this.setupClonedElement(t,e)}});return l.validateCanvas(r),{width:e.offsetWidth,height:e.offsetHeight,canvasData:r.toDataURL(`image/jpeg`,t.quality||.75),canvas:r}}catch(e){throw l.createError(`CANVAS_GENERATION_FAILED`,`Canvas生成失败`,e)}}static setupClonedElement(e,t){let n=e.getElementById(t.id);n&&(n.style.backgroundColor=`#ffffff`,n.style.color=`#000000`,n.style.fontFamily=`Arial, sans-serif`,n.style.fontSize=`14px`,n.style.lineHeight=`1.5`)}static async convertDomToCanvas(e,t={}){return this.validateElementSize(e),this.setupElementStyles(e),await this.convertWithHtml2Canvas(e,t)}static validateElementSize(e){if(e.offsetWidth===0||e.offsetHeight===0)throw l.createError(`EMPTY_ELEMENT`,`元素尺寸为0,无法转换`)}static setupElementStyles(e){e.style.backgroundColor=`#ffffff`,e.style.color=`#000000`,e.style.fontFamily=`Arial, sans-serif`,e.style.fontSize=`14px`,e.style.lineHeight=`1.5`}static validateCanvasSize(e,t){l.checkCanvasSize(e,t)}static getCanvasDataUrl(e,t=.75){return e.toDataURL(`image/jpeg`,t)}},k=class{static timers=new Map;static logs=[];static isEnabled=!1;static enable(){this.isEnabled=!0}static disable(){this.isEnabled=!1}static log(...e){this.isEnabled&&console.log(`💥`,...e)}static startTimer(e){this.isEnabled&&(this.timers.set(e,Date.now()),console.log(`🚀 开始执行: ${e}`))}static endTimer(e,t=!1){if(!this.isEnabled)return 0;let n=this.timers.get(e);if(!n)return console.warn(`⚠️ 未找到计时器: ${e}`),0;let r=Date.now()-n,i=this.getMemoryUsage();this.timers.delete(e),this.logs.push({step:e,duration:r,timestamp:Date.now(),memory:i});let a=t?`
🎉 👏 `:`✅`,o=i?`,内存: ${i}MB`:``;return console.log(`${a} ${e} 完成,耗时: ${r}ms${o}`),r}static getMemoryUsage(){if(`memory`in performance){let e=performance.memory;return Math.round(e.usedJSHeapSize/1024/1024*100)/100}}static getTotalTime(){if(this.logs.length===0)return 0;let e=this.logs[0],t=this.logs[this.logs.length-1];return t.timestamp-e.timestamp}static getReport(){let e=this.getTotalTime(),t=this.logs.map(t=>({step:t.step,duration:t.duration,percentage:e>0?Math.round(t.duration/e*100):0,memory:t.memory})),n=[...t].sort((e,t)=>t.duration-e.duration),r=t.length>0?Math.round(t.reduce((e,t)=>e+t.duration,0)/t.length):0;return{totalTime:e,steps:t,averageTime:r,slowestStep:n[0]?.step||``,fastestStep:n[n.length-1]?.step||``}}static printSummary(){if(!this.isEnabled)return;let e=this.getReport();console.log(`
📊 性能监控报告:`),console.log(`=`.repeat(60)),console.log(`总耗时: ${e.totalTime}ms`),console.log(`平均耗时: ${e.averageTime}ms`),console.log(`最慢步骤: ${e.slowestStep}`),console.log(`最快步骤: ${e.fastestStep}`),console.log(``),console.log(`详细步骤:`),e.steps.forEach(e=>{let t=e.memory?` (内存: ${e.memory}MB)`:``;console.log(` ${e.step}: ${e.duration}ms (${e.percentage}%)${t}`)}),console.log(`=`.repeat(60))}static reset(){this.timers.clear(),this.logs=[]}static getLogs(){return[...this.logs]}static exportData(){return JSON.stringify({timestamp:Date.now(),report:this.getReport(),logs:this.getLogs()},null,2)}},A=class{options;paperSize;constructor(e){k.reset(),this.options=this.mergeOptions(e),this.options.enablePerformanceMonitor&&k.enable(),this.paperSize=this.calculatePaperSize()}mergeOptions(e){return{...r,...e,pageMargin:{...r.pageMargin,...e.pageMargin},html2canvas:{scale:1.5,quality:.75,useCORS:!0,allowTaint:!0,backgroundColor:`#ffffff`,logging:!1,...e.html2canvas},watermark:{content:``,color:`#ddd`,size:14,angle:45,opacity:.2,...e.watermark}}}calculatePaperSize(){let e=D.getPaperSize(this.options.format||`a4`),t=this.options.orientation===`p`||this.options.orientation===`portrait`;return t?e:{width:e.height,height:e.width}}calculatePageDimensions(e){k.startTimer(`计算页面尺寸和边距`);let{top:t=10,bottom:n=10,left:r=10,right:i=10}=this.options.pageMargin||{top:10,bottom:10,left:10,right:10},a=this.paperSize.width-r-i,o=this.paperSize.height-t-n,s=e.offsetWidth,c=s/a*o;return k.endTimer(`计算页面尺寸和边距`),{marginTop:t,marginBottom:n,marginLeft:r,marginRight:i,contentWidth:a,contentHeight:o,pageWidth:s,pageHeight:c}}getPaperSize=e=>D.getPaperSize(e);generatePDF=async()=>{k.startTimer(`PDF生成总流程`);try{k.startTimer(`获取DOM元素`);let e=u.getElement(this.options.elementId);l.validateElement(e,this.options.elementId),k.endTimer(`获取DOM元素`);let t=this.calculatePageDimensions(e),n=e.scrollHeight,r=u.createTempContainer(t.pageWidth.toString()),i=u.cloneElement(e);r.appendChild(i);let a=u.processNonBreakableElements({tempContainer:r,pageHeight:t.pageHeight,wholeNodeClass:this.options.wholeNodeClass,intervalHeight:this.options.intervalHeight}),o=n+a;r.style.height=`${o}px`,k.startTimer(`DOM转Canvas`);let{canvas:s}=await O.convertDomToCanvas(r,this.options.html2canvas);k.endTimer(`DOM转Canvas`),k.startTimer(`创建PDF文档`);let{blob:c,totalPages:d}=await D.createPdfDocument({canvas:s,contentWidth:t.contentWidth,contentHeight:t.contentHeight,margins:{left:t.marginLeft,top:t.marginTop,bottom:t.marginBottom,right:t.marginRight},pdf:{orientation:this.options.orientation||`p`,format:this.options.format||`a4`,quality:this.options.html2canvas?.quality||.75},headerFooter:{header:this.options.header,footer:this.options.footer},watermark:this.options.watermark,file:{fileName:this.options.fileName,open:this.options.open}});return k.endTimer(`创建PDF文档`),u.cleanupTempContainer(r),k.endTimer(`PDF生成总流程`,!0),k.log(`PDF生成信息`,{size:`${(c.size/1024/1024).toFixed(2)}MB`,pages:d,orientation:this.options.orientation,format:this.options.format}),k.printSummary(),c}catch(e){throw k.endTimer(`PDF生成总流程`),console.error(`❌ PDF生成失败:`,e),e}};startTimer=e=>k.startTimer(e);endTimer=(e,t=!1)=>k.endTimer(e,t);getReport=()=>k.getReport();printSummary=()=>k.printSummary();getLogs=()=>k.getLogs();exportData=()=>k.exportData();reset=()=>k.reset()},j=A;export{t as DEFAULT_FONT,r as DEFAULT_OPTIONS,s as ERROR_CODES,a as LIMITS,i as PAPER_SIZES,c as PERFORMANCE_THRESHOLDS,o as STYLES,n as WATERMARK_REPEATS,j as default};