/** 将可打印 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();
}
}