Files
qhmes/jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts

221 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 将可打印 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<string> {
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 = `<div class="lodop-print-root" style="width:${widthMm}mm;min-width:${widthMm}mm;margin:0;padding:0;background:#fff;overflow:visible;box-sizing:border-box;">${html}</div>`;
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();
}
}