新增PrintDot桥接功能,支持本地打印机连接和配置,优化打印模板设计,允许多页表格重复显示,改进打印预览和设计器界面,确保用户体验流畅。
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/** 将可打印 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 [{ 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 = Math.max(widthMm, pxToMm(sw) + pad);
|
||||
const sheetH = Math.max(1, heightMm);
|
||||
const sliceH = Math.max(1, Math.round(mmToPx(sheetH) * scale));
|
||||
const pdf = new jsPDF({ unit: 'mm', 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 = 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 pdf = new jsPDF({ unit: 'mm', 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user