/** 将可打印 HTML 片段(非完整文档)转为 PDF Base64,供 PrintDot / 后端队列等使用 */ function mmToPx(mm: number) { return (mm * 96) / 25.4; } function pxToMm(px: number) { return (px * 25.4) / 96; } /** * 原生模板大量 position:absolute,子块可能低于父级 scrollHeight; * 用后代包围盒与 scroll 尺寸取大,避免栅格化高度不足、末页空白。 */ function measureCaptureExtentPx(root: HTMLElement): { sw: number; sh: number } { const base = root.getBoundingClientRect(); let maxR = 0; let maxB = 0; const visit = (el: Element) => { const r = (el as HTMLElement).getBoundingClientRect(); maxR = Math.max(maxR, r.right - base.left); maxB = Math.max(maxB, r.bottom - base.top); Array.from(el.children).forEach(visit); }; visit(root); const sw = Math.max(1, Math.ceil(maxR), root.scrollWidth, root.clientWidth); const sh = Math.max(1, Math.ceil(maxB), root.scrollHeight, root.clientHeight); return { sw, sh }; } function arrayBufferToBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); const chunkSize = 0x8000; let binary = ''; for (let i = 0; i < bytes.length; i += chunkSize) { const chunk = bytes.subarray(i, i + chunkSize); binary += String.fromCharCode(...chunk); } return btoa(binary); } /** 从 renderNativePrintHtml 等生成的完整 HTML 文档中取出 body 内层,便于套入与 hiprint 相同的 PDF 容器逻辑 */ export function extractBodyInnerHtmlFromFullDocument(fullHtml: string): string { try { const doc = new DOMParser().parseFromString(fullHtml, 'text/html'); const inner = doc.body?.innerHTML?.trim(); return inner || fullHtml; } catch { return fullHtml; } } export type BuildPdfFromHtmlOptions = { /** * true(默认):widthMm/heightMm 为单张纸宽高,按高度纵向切片为多页 PDF,对齐浏览器打印分页。 * false:整张版面压成一页 PDF(长图一页,一般仅特殊场景使用)。 */ paginate?: boolean; /** * 是否严格使用入参纸张尺寸(默认 false 保持历史行为)。 * 原生模板桥接打印建议开启,避免内容测量误差把小标签纸扩成 A4。 */ exactPaperSize?: boolean; }; /** * @param html 内部 HTML 片段(无外层 html/body) * @param widthMm 纸张宽度 mm(分页模式下每页同宽) * @param heightMm 纸张高度 mm;分页模式下为「每一页」高度;非分页时为版心高度下限 */ export async function buildPdfBase64FromHtmlFragment( html: string, widthMm: number, heightMm: number, options: BuildPdfFromHtmlOptions = {}, ): Promise { const paginate = options.paginate !== false; const exactPaperSize = options.exactPaperSize === true; const [{ jsPDF }, html2canvasModule] = await Promise.all([import('jspdf'), import('html2canvas')]); const html2canvas = html2canvasModule.default; const container = document.createElement('div'); container.style.position = 'fixed'; container.style.left = '-20000px'; container.style.top = '0'; container.style.width = 'auto'; container.style.maxWidth = 'none'; container.style.overflow = 'visible'; container.style.background = '#fff'; container.style.zIndex = '-1'; container.innerHTML = `
${html}
`; document.body.appendChild(container); try { const target = (container.querySelector('.lodop-print-root') || container) as HTMLElement; await new Promise((resolve) => setTimeout(resolve, 400)); const measure = () => { const sw = Math.max(target.scrollWidth, target.clientWidth, 1); const sh = Math.max(target.scrollHeight, target.clientHeight, 1); return { sw, sh }; }; let { sw, sh } = measure(); const minWpx = mmToPx(Math.max(1, widthMm)); if (sw < minWpx) { target.style.width = `${minWpx}px`; } if (sw > minWpx) { target.style.width = `${sw}px`; } ({ sw, sh } = measure()); let ext = measureCaptureExtentPx(target); sw = Math.max(sw, ext.sw); sh = Math.max(sh, ext.sh); target.style.minHeight = `${sh}px`; target.style.width = `${Math.max(minWpx, sw)}px`; ({ sw, sh } = measure()); ext = measureCaptureExtentPx(target); sw = Math.max(sw, ext.sw); sh = Math.max(sh, ext.sh); const MAX_EDGE = 12000; let scale = 2; while (Math.max(sw, sh) * scale > MAX_EDGE && scale > 0.75) { scale -= 0.25; } const canvas = await html2canvas(target, { backgroundColor: '#ffffff', scale, useCORS: true, allowTaint: true, logging: false, width: sw, height: sh, windowWidth: sw, windowHeight: sh, onclone: (_clonedDoc, clonedEl) => { const root = (clonedEl.querySelector?.('.lodop-print-root') as HTMLElement) || (clonedEl as HTMLElement); if (!root) { return; } root.style.minHeight = `${sh}px`; root.style.width = `${sw}px`; root.style.overflow = 'visible'; const relaxOverflow = (el: HTMLElement) => { const st = el.style; if (st.overflow === 'hidden' || st.overflowX === 'hidden' || st.overflowY === 'hidden') { st.setProperty('overflow', 'visible', 'important'); } Array.from(el.children).forEach((c) => relaxOverflow(c as HTMLElement)); }; relaxOverflow(root); }, }); const cw = Math.max(1, canvas.width); const ch = Math.max(1, canvas.height); const pad = 1; if (paginate) { const sheetW = exactPaperSize ? Math.max(1, widthMm) : Math.max(widthMm, pxToMm(sw) + pad); const sheetH = Math.max(1, heightMm); const sliceH = Math.max(1, Math.round(mmToPx(sheetH) * scale)); const orientation = sheetW > sheetH ? 'landscape' : 'portrait'; const pdf = new jsPDF({ unit: 'mm', orientation, format: [sheetW, sheetH] }); let y = 0; let first = true; /** 余量不足一页高的 2% 时视为测量噪声,避免多出一页空白 */ const tailNoisePx = Math.max(8, Math.floor(sliceH * 0.02)); while (y < ch) { const remain = ch - y; if (!first && remain <= tailNoisePx) { break; } const hPx = Math.min(sliceH, remain); if (hPx < 1) { break; } const pageCanvas = document.createElement('canvas'); pageCanvas.width = cw; pageCanvas.height = hPx; const ctx = pageCanvas.getContext('2d'); if (ctx) { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, cw, hPx); ctx.drawImage(canvas, 0, y, cw, hPx, 0, 0, cw, hPx); } const imgData = pageCanvas.toDataURL('image/jpeg', 0.92); const imgHmm = sheetH * (hPx / sliceH); if (!first) { pdf.addPage(); } first = false; pdf.addImage(imgData, 'JPEG', 0, 0, sheetW, imgHmm); y += sliceH; } return arrayBufferToBase64(pdf.output('arraybuffer')); } // 单页长图模式(paginate: false) const contentWidthMm = pxToMm(sw); const contentHeightMm = pxToMm(sh); const minW = exactPaperSize ? Math.max(1, widthMm) : Math.max(widthMm, contentWidthMm) + pad; const minH = Math.max(heightMm, contentHeightMm) + pad; const canvasRatio = cw / ch; let pdfH = Math.max(minH, minW / canvasRatio); let pdfW = pdfH * canvasRatio; if (pdfW < minW) { pdfW = minW; pdfH = pdfW / canvasRatio; } const orientation = pdfW > pdfH ? 'landscape' : 'portrait'; const pdf = new jsPDF({ unit: 'mm', orientation, format: [pdfW, pdfH] }); const imgData = canvas.toDataURL('image/jpeg', 0.92); pdf.addImage(imgData, 'JPEG', 0, 0, pdfW, pdfH); return arrayBufferToBase64(pdf.output('arraybuffer')); } finally { container.remove(); } }