init commit
This commit is contained in:
166
src/pdfExport.ts
Normal file
166
src/pdfExport.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import type { PaperConfig, LabelTemplate, CutLineConfig } from './types';
|
||||
import type { PrintableLabel } from './utils';
|
||||
|
||||
export interface PdfExportProgress {
|
||||
done: number;
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
phase: 'render' | 'save';
|
||||
}
|
||||
|
||||
export interface AsyncExportLabelsPdfOptions {
|
||||
paper: PaperConfig;
|
||||
template: LabelTemplate;
|
||||
printablePages: (PrintableLabel | null)[][];
|
||||
gridCols: number;
|
||||
cutLine: CutLineConfig;
|
||||
filename?: string;
|
||||
imageFormat: 'PNG' | 'JPEG';
|
||||
renderLabel: (label: PrintableLabel) => Promise<string>;
|
||||
onProgress?: (progress: PdfExportProgress) => void;
|
||||
}
|
||||
|
||||
const yieldToMain = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const PAGE_RENDER_CONCURRENCY = 3;
|
||||
|
||||
function parseColorToRgb(color: string): [number, number, number] {
|
||||
const c = color.trim();
|
||||
if (c.startsWith('#')) {
|
||||
const hex = c.slice(1);
|
||||
if (hex.length === 3) {
|
||||
return [
|
||||
parseInt(hex[0] + hex[0], 16),
|
||||
parseInt(hex[1] + hex[1], 16),
|
||||
parseInt(hex[2] + hex[2], 16),
|
||||
];
|
||||
}
|
||||
if (hex.length >= 6) {
|
||||
return [
|
||||
parseInt(hex.slice(0, 2), 16),
|
||||
parseInt(hex.slice(2, 4), 16),
|
||||
parseInt(hex.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
}
|
||||
return [204, 204, 204];
|
||||
}
|
||||
|
||||
function drawCutLineRect(
|
||||
pdf: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
cutLine: CutLineConfig
|
||||
) {
|
||||
const [r, g, b] = parseColorToRgb(cutLine.color);
|
||||
pdf.setDrawColor(r, g, b);
|
||||
pdf.setLineWidth(Math.max(0.02, cutLine.width));
|
||||
if (cutLine.style === 'dashed') {
|
||||
const dash = Math.max(0.5, cutLine.width * 4);
|
||||
pdf.setLineDashPattern([dash, dash], 0);
|
||||
} else {
|
||||
pdf.setLineDashPattern([], 0);
|
||||
}
|
||||
pdf.rect(x, y, w, h, 'S');
|
||||
pdf.setLineDashPattern([], 0);
|
||||
}
|
||||
|
||||
/** 按页流式渲染并写入 PDF,避免大批量时内存暴涨与长时间阻塞 */
|
||||
export async function exportLabelsPdfAsync(options: AsyncExportLabelsPdfOptions): Promise<void> {
|
||||
const {
|
||||
paper,
|
||||
template,
|
||||
printablePages,
|
||||
gridCols,
|
||||
cutLine,
|
||||
filename = 'labels.pdf',
|
||||
imageFormat,
|
||||
renderLabel,
|
||||
onProgress,
|
||||
} = options;
|
||||
|
||||
const paperW = paper.orientation === 'portrait' ? paper.width : paper.height;
|
||||
const paperH = paper.orientation === 'portrait' ? paper.height : paper.width;
|
||||
const orientation = paperW > paperH ? 'landscape' : 'portrait';
|
||||
const labelW = template.width;
|
||||
const labelH = template.height;
|
||||
const totalPages = printablePages.length;
|
||||
|
||||
const totalLabels = printablePages.reduce(
|
||||
(sum, page) => sum + page.filter((item) => item !== null).length,
|
||||
0
|
||||
);
|
||||
|
||||
const pdf = new jsPDF({
|
||||
orientation,
|
||||
unit: 'mm',
|
||||
format: [paperW, paperH],
|
||||
compress: true,
|
||||
});
|
||||
|
||||
let done = 0;
|
||||
let progressTick = 0;
|
||||
|
||||
const report = (page: number, phase: PdfExportProgress['phase']) => {
|
||||
onProgress?.({ done, total: totalLabels, page, totalPages, phase });
|
||||
};
|
||||
|
||||
for (let pIdx = 0; pIdx < printablePages.length; pIdx++) {
|
||||
if (pIdx > 0) {
|
||||
pdf.addPage([paperW, paperH], orientation);
|
||||
}
|
||||
|
||||
const pageRows = printablePages[pIdx];
|
||||
report(pIdx + 1, 'render');
|
||||
|
||||
const pageJobs: { cellIdx: number; label: PrintableLabel }[] = [];
|
||||
for (let cellIdx = 0; cellIdx < pageRows.length; cellIdx++) {
|
||||
const item = pageRows[cellIdx];
|
||||
if (!item) continue;
|
||||
pageJobs.push({ cellIdx, label: item });
|
||||
}
|
||||
|
||||
for (let i = 0; i < pageJobs.length; i += PAGE_RENDER_CONCURRENCY) {
|
||||
const batch = pageJobs.slice(i, i + PAGE_RENDER_CONCURRENCY);
|
||||
const rendered = await Promise.all(
|
||||
batch.map(async (job) => ({
|
||||
...job,
|
||||
dataUrl: await renderLabel(job.label),
|
||||
}))
|
||||
);
|
||||
|
||||
for (const { cellIdx, dataUrl } of rendered) {
|
||||
const col = cellIdx % gridCols;
|
||||
const row = Math.floor(cellIdx / gridCols);
|
||||
const x = paper.marginLeft + col * (labelW + paper.columnGap);
|
||||
const y = paper.marginTop + row * (labelH + paper.rowGap);
|
||||
|
||||
pdf.addImage(dataUrl, imageFormat, x, y, labelW, labelH, undefined, 'FAST');
|
||||
|
||||
if (cutLine.enabled) {
|
||||
drawCutLineRect(pdf, x, y, labelW, labelH, cutLine);
|
||||
}
|
||||
|
||||
done++;
|
||||
}
|
||||
|
||||
progressTick++;
|
||||
if (progressTick >= 2) {
|
||||
progressTick = 0;
|
||||
report(pIdx + 1, 'render');
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
report(pIdx + 1, 'render');
|
||||
await yieldToMain();
|
||||
}
|
||||
|
||||
report(totalPages, 'save');
|
||||
await yieldToMain();
|
||||
pdf.save(filename);
|
||||
}
|
||||
Reference in New Issue
Block a user