关键词:SumatraPDF, Web打印, 静默打印, web-print-pdf, npm包, Node.js, Electron, 前端打印, PDF打印, 无预览打印, 自动化打印, 企业级打印, 打印解决方案
摘要:本文深入分析了SumatraPDF作为Web静默打印引擎的技术实现,重点介绍了web-print-pdf npm包如何巧妙集成SumatraPDF实现无预览静默打印功能。文章涵盖了技术架构、实现原理、配置参数和实际应用,为前端开发者提供了完整的Web静默打印解决方案。
在现代Web应用开发中,静默打印是一个重要的技术需求,特别是在企业级应用中需要自动化打印的场景。传统的Web打印方案存在用户交互、兼容性差等问题,而SumatraPDF作为轻量级的PDF阅读器,其强大的命令行打印能力为Web静默打印提供了完美的解决方案。
笔者在实际项目开发中,深入研究了如何将SumatraPDF集成到Web打印系统中,通过web-print-pdf npm包实现了完整的静默打印功能。本文将分享这些实践经验,帮助开发者理解SumatraPDF在Web打印中的重要作用,以及如何通过web-print-pdf npm包轻松实现静默打印功能。
SumatraPDF是一个开源的PDF阅读器,以其轻量级、高性能著称:
SumatraPDF提供了丰富的命令行参数,支持各种打印需求:
# 基础静默打印
SumatraPDF.exe -print-to-default -silent document.pdf
# 指定打印机静默打印
SumatraPDF.exe -print-to "HP LaserJet Pro" -silent document.pdf
# 高级打印设置
SumatraPDF.exe -print-to "HP LaserJet Pro" -print-settings "2x,duplex,color" -silent document.pdf
web-print-pdf npm包作为现代Web打印解决方案,巧妙地集成了SumatraPDF作为其核心打印引擎:
// web-print-pdf npm包的核心架构
import webPrintPdf from 'web-print-pdf';
// 前端开发者只需要关注业务逻辑
const silentPrintExample = async () => {
try {
// web-print-pdf npm包内部自动调用SumatraPDF进行静默打印
const result = await webPrintPdf.printHtml(
'<h1>测试文档</h1><p>这是静默打印的内容</p>',
{
// PDF配置
paperFormat: 'A4',
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' },
printBackground: true
},
{
// 打印配置 - 这些参数会转换为SumatraPDF命令行参数
printerName: 'HP LaserJet Pro', // 转换为 -print-to 参数
copies: 2, // 转换为 2x 参数
duplexMode: 'duplex', // 转换为 duplex 参数
colorful: true // 转换为 color 参数
}
);
console.log('静默打印成功:', result);
return result;
} catch (error) {
console.error('静默打印失败:', error);
throw error;
}
};
web-print-pdf npm包智能地将前端配置转换为SumatraPDF命令行参数:
// 前端配置
const printOptions = {
printerName: 'HP LaserJet Pro',
copies: 3,
duplexMode: 'duplex',
colorful: true,
pageRanges: [{from: 1, to: 5}, {from: 7, to: 10}]
};
// web-print-pdf npm包内部转换为SumatraPDF命令行
// SumatraPDF.exe -print-to "HP LaserJet Pro" -print-settings "3x,duplex,color,1-5,7-10" -silent document.pdf
web-print-pdf npm包自动管理SumatraPDF的下载、安装和配置:
// 自动检查SumatraPDF环境
const checkEnvironment = async () => {
try {
// web-print-pdf npm包自动检查SumatraPDF是否可用
const status = await webPrintPdf.utils.getConnectStatus();
if (!status) {
console.log('正在启动打印专家客户端...');
// 自动下载和配置SumatraPDF
return false;
}
return true;
} catch (error) {
console.error('环境检查失败:', error);
return false;
}
};
基于对项目代码的分析,SumatraPDF的集成采用了以下架构:
class Printer {
constructor() {
this.sumatraPdfPath = this._getSumatraPdfPath();
}
// 解析打印参数,转换为SumatraPDF命令行
_parseParams(pdfPath, printOptions = {}) {
const {printerName, ...printSettings} = printOptions;
const args = [];
// 打印机选择
if (printerName) {
args.push("-print-to", printerName);
} else {
args.push("-print-to-default");
}
// 静默模式
args.push("-silent");
// 打印设置参数
const printSettingsArgs = [];
// 打印份数
if (printSettings.copies) {
printSettingsArgs.push(`${printSettings.copies}x`);
}
// 双面打印
if (printSettings.duplexMode) {
printSettingsArgs.push(printSettings.duplexMode);
}
// 彩色/黑白
if (printSettings.colorful) {
printSettingsArgs.push("color");
} else {
printSettingsArgs.push("monochrome");
}
// 页码范围
if (printSettings.pageRanges) {
const ranges = printSettings.pageRanges
.map(item => `${item.from}-${item.to}`)
.join(',');
printSettingsArgs.push(ranges);
}
// 纸盘选择
if (printSettings.bin) {
printSettingsArgs.push(`bin=${printSettings.bin}`);
}
// 组合所有参数
if (printSettingsArgs.length) {
args.push("-print-settings", printSettingsArgs.join(","));
}
args.push(pdfPath);
return args;
}
// 执行打印任务
async _print(pdfPath, printOptions = {}, extraOptions = {}) {
try {
const args = this._parseParams(pdfPath, printOptions);
// 通过子进程调用SumatraPDF
const result = await this._executeSumatraPdf(args);
return {
success: true,
pdfPath,
pdfFileName: path.basename(pdfPath)
};
} catch (error) {
return {
success: false,
pdfPath,
msg: error.message
};
}
}
}
项目使用Node.js的child_process模块来管理SumatraPDF进程,实现了完整的进程生命周期管理:
class ChildProcessPromise {
constructor() {
this.processStartNum = 0;
this.processForceCloseNum = 0;
this.processAutoCloseNum = 0;
}
resetNum() {
this.processStartNum = 0;
this.processForceCloseNum = 0;
this.processAutoCloseNum = 0;
}
// 检查打印机错误状态
async _checkPrinterErrorStatus(args) {
if (!args) return;
try {
const findIndex = args?.findIndex(one => one === '-print-to');
let printer = null;
if (findIndex > -1) {
printer = {
name: args[findIndex + 1]
}
} else {
const printers = await systemPrinter.getPrinterList();
printer = printers?.find(one => one.default)
}
if (printer?.name) {
const res = await systemPrinter.getAbnormalTask(printer);
if (res.length) {
noticeWarn.notification(lang('打印机异常提醒'),
app._lang === 'en' ?
`Printing timeout, detected ${res.length} abnormal task statuses in the current printer, usually due to incorrect or blocked tasks not being executed.Please check the printer task list and clear it` :
`打印超时,检测到当前打印机有${res.length}条任务状态异常,通常是由于错误的或阻塞的任务未执行掉,请检查打印机任务列表并清除它`);
}
}
} catch (err) {
console.error(err)
}
}
async spawnExeProcess(exePath, args, spanName = '') {
return new Promise((resolve, reject) => {
const taskId = uuid();
const stdoutErrors = [] // stdout出现的错误记录器
let closed = false; // 进程是否关闭
// 子进程
let spawnProcess;
// 定时器监听
let updateTime = Date.now();
let timeout = null;
let _setTimeout;
let _deadTimeout = 0;
const clear_SetTimeout = () => {
clearTimeout(timeout);
_setTimeout = null;
}
// 停止进程函数
const stopProcess = () => {
clear_SetTimeout();
try {
appProgress.clearPrintFileTask(taskId);
} catch (err) {
console.error(`appProgress.clearPrintFileTask 清除进度出错:${err?.message}`)
}
if (spawnProcess && spawnProcess.pid && spawnProcess.exitCode === null) {
try {
spawnProcess.kill && spawnProcess.kill(); // 必须有pid 才能kill
} catch (err) {
console.error(err)
}
if (!closed) {
this.processForceCloseNum += 1
console.log(`(${spanName})子进程已强制关闭,pid:`, spawnProcess.pid);
}
} else {
if (!closed) {
this.processAutoCloseNum += 1
console.log(`(${spanName})子进程已自动关闭,pid:`, spawnProcess?.pid);
}
}
closed = true;
}
const onResolve = () => {
resolve(true)
}
const onReject = (err) => {
reject(err)
}
// 超时检测机制
_setTimeout = async () => {
try {
clearTimeout(timeout);
timeout = setTimeout(async () => {
if (!spawnProcess?.pid) {
clear_SetTimeout();
return;
}
if (Date.now() - updateTime > 5 * 1000) {
const timeoutCause = 'it is usually due to an error or blocked task not being executed by the printer. Please check the printer task list and clear it'
await pidusage(spawnProcess.pid).then(res => {
const {cpu, memory} = res;
if (+cpu === 0) {
_deadTimeout = _deadTimeout + 3;
}
if (_deadTimeout > 8) {
stopProcess();
this._checkPrinterErrorStatus(args);
onReject(`timeout stopped for have no progress when sending to printer, ${timeoutCause}`);
} else {
_setTimeout && _setTimeout()
}
}).catch((err) => {
if (err?.message.includes('No matching pid found')) {
console.error('catch a known error: No matching pid found, this is not important!')
} else {
console.error('pidusage unCatch error:', err);
if (Date.now() - updateTime > 300 * 1000) {
// 彻底坏了,5分钟一定要关闭
stopProcess();
this._checkPrinterErrorStatus(args);
onReject(`timeout stopped for have no progress when sending to printer over 5 minutes: ${err?.message}, ${timeoutCause}`);
}
return Promise.reject(err)
}
})
} else {
_setTimeout && _setTimeout();
}
}, 3000)
} catch (err) {
console.error(`进程cpu监听定时器报错:${err}`)
}
};
try {
spawnProcess = childProcess.spawn(exePath, args, {
windowsHide: true
})
this.processStartNum += 1;
if (!spawnProcess.pid) {
stopProcess();
onReject('create child process failed,pid is not exist!');
return;
}
console.log(`(${spanName})子进程创建,pid:`, spawnProcess.pid);
_setTimeout();
try {
appProgress.updatePrintFileTask(taskId);
} catch (err) {
console.error(`appProgress.updatePrintFileTask 记录进度出错:${err?.message}`)
}
// 监听进程输出
spawnProcess.stdout.on('data', (data) => {
if (data) {
updateTime = Date.now();
_deadTimeout = 0;
}
if (data) {
// 分析记录,记录一些错误记录
const str = `${data}`
if (str.includes('PrintToDevice: failed')) {
stdoutErrors.push({
type: 0,
message: str + '。If you cancel actively, no need to pay attention',
})
}
if (str.includes('Error: Couldn\'t open file')) {
stdoutErrors.push({
type: 1,
message: str,
})
}
}
});
// 监听进程错误
spawnProcess.on("error", (error) => {
stopProcess();
onReject(error);
});
// 监听进程关闭
spawnProcess.on("close", async (code, err) => {
stopProcess();
if (![0].includes(code)) {
let standerMsg = `printing exited with exit code: ${code}`
const findType1 = stdoutErrors.find(one => one.type === 1)
if (findType1) {
const pdfPath = args.at(-1);
try {
await paramsValid.validPdfDecrypt(pdfPath)
} catch (err) {
findType1.message = err.message
}
}
if (stdoutErrors.length) {
const errors = stdoutErrors.map(item => item.message).join(';');
standerMsg = standerMsg + ', because some reasons:' + errors?.replace('.:', ',')
}
onReject(standerMsg)
} else {
onResolve();
}
});
} catch (err) {
stopProcess();
onReject(err);
}
})
}
}
项目实现了SumatraPDF的自动下载和配置:
class RequiredEnv {
constructor() {
this.PDFExePath = path.join(app.getPath('userData'), 'SumatraPDF.exe');
this.isDownloadingPrintPdfExe = false;
this.isDownloadingPrintPdfExeTries = 0;
}
// 检查SumatraPDF是否已安装
async _checkPrintPdf64exe() {
const isExist = await fs.pathExists(this.PDFExePath);
if (!isExist) {
// 通知前端开始下载
app._win.webContents.send('getRequiredEnvDownloading');
if (!this.isDownloadingPrintPdfExe) {
if (this.isDownloadingPrintPdfExeTries < 3) {
// 自动下载SumatraPDF
this._downloadPrintPdf64exe(this.PDFExePath);
} else {
app._win.webContents.send('global/warning', '核心组件无法下载,请联系技术解决');
}
}
// 定时重试检查
this.isDownloadingPrintPdfExeInterval = setTimeout(
() => this._checkPrintPdf64exe(),
6000
);
} else {
// 通知前端下载成功
app._win.webContents.send('getRequiredEnvDownloadingSuccess', this.typeEnumes.pdfExe);
}
}
// 下载SumatraPDF
async _downloadPrintPdf64exe(targetPath) {
this.isDownloadingPrintPdfExe = true;
try {
// 从多个源下载,确保可用性
const downloadUrls = [
'https://files2.sumatrapdfreader.org/software/sumatrapdf/rel/3.5.2/SumatraPDF-3.5.2-64.zip',
'http://webprintpdf.com/api/fileCenter/webPrintExpert/fileCenterFileDownload/SumatraPDF-3.5.2-64.zip'
];
// 下载并解压
await this._downloadAndExtract(downloadUrls, targetPath);
} catch (error) {
this.isDownloadingPrintPdfExeTries++;
console.error('SumatraPDF下载失败:', error);
} finally {
this.isDownloadingPrintPdfExe = false;
}
}
}
# 基本静默打印
SumatraPDF.exe -print-to-default -silent document.pdf
# 指定打印机
SumatraPDF.exe -print-to "HP LaserJet Pro" -silent document.pdf
# 打印到文件
SumatraPDF.exe -print-to-file "output.pdf" -silent document.pdf
# 打印份数
SumatraPDF.exe -print-settings "2x" -silent document.pdf
# 双面打印
SumatraPDF.exe -print-settings "duplex" -silent document.pdf
# 彩色打印
SumatraPDF.exe -print-settings "color" -silent document.pdf
# 组合设置
SumatraPDF.exe -print-settings "2x,duplex,color" -silent document.pdf
# 页码范围
SumatraPDF.exe -print-settings "1-5,7-10" -silent document.pdf
# 纸盘选择
SumatraPDF.exe -print-settings "bin=1" -silent document.pdf
# 缩放模式
SumatraPDF.exe -print-settings "shrink" -silent document.pdf
web-print-pdf npm包将复杂的SumatraPDF命令行调用封装成简单的JavaScript API:
// 传统方式:直接调用SumatraPDF命令行
// 需要手动构建命令行参数,处理进程管理,错误处理等
// web-print-pdf npm包方式:简洁的JavaScript API
const result = await webPrintPdf.printHtml(
htmlContent,
{ paperFormat: 'A4' },
{
printerName: 'HP LaserJet Pro',
copies: 2,
duplexMode: 'duplex'
}
);
// 订单自动打印服务
class OrderAutoPrintService {
async printOrder(orderData) {
const orderHtml = this.generateOrderHtml(orderData);
// web-print-pdf npm包内部调用SumatraPDF进行静默打印
return await webPrintPdf.printHtml(
orderHtml,
{
// PDF配置
paperFormat: 'A5',
margin: { top: '10mm', bottom: '10mm', left: '10mm', right: '10mm' }
},
{
// 打印配置
silent: true, // 静默打印
printerName: orderData.printerName || 'default',
copies: orderData.copies || 1,
duplexMode: 'simplex' // 单面打印
}
);
}
generateOrderHtml(orderData) {
return `
<div style="font-family: Arial, sans-serif; padding: 20px;">
<h1 style="text-align: center; color: #333;">订单确认单</h1>
<div style="border: 1px solid #ddd; padding: 15px; margin: 20px 0;">
<p><strong>订单号:</strong>${orderData.orderNo}</p>
<p><strong>客户名称:</strong>${orderData.customerName}</p>
<p><strong>联系电话:</strong>${orderData.phone}</p>
<p><strong>订单金额:</strong>¥${orderData.amount}</p>
<p><strong>打印时间:</strong>${new Date().toLocaleString()}</p>
</div>
<div style="text-align: center; margin-top: 30px;">
<p>感谢您的购买!</p>
</div>
</div>
`;
}
}
// 报表批量打印服务
class ReportBatchPrintService {
async printReports(reports) {
const printTasks = reports.map(report => ({
content: this.generateReportHtml(report),
pdfOptions: {
paperFormat: 'A4',
printBackground: true,
watermark: {
text: '机密文件',
color: 'rgb(255,0,0)',
opacity: 0.3
},
pageNumber: {
format: '第{{page}}页/共{{totalPage}}页',
x: 'alignCenter',
y: 'alignBottom'
}
},
printOptions: {
silent: true,
duplexMode: 'duplex',
copies: report.copies || 1,
printerName: report.printerName || 'default'
}
}));
try {
// web-print-pdf npm包内部调用SumatraPDF进行批量打印
const result = await webPrintPdf.batchPrint(printTasks);
console.log('批量报表打印完成:', result);
return result;
} catch (error) {
console.error('批量报表打印失败:', error);
throw error;
}
}
}
// 发票自动打印服务
class InvoiceAutoPrintService {
async printInvoice(invoiceData) {
const invoiceHtml = this.generateInvoiceHtml(invoiceData);
// 自动选择发票打印机
const printerName = await this.getInvoicePrinter();
return await webPrintPdf.printHtml(
invoiceHtml,
{
// 发票专用纸张格式
paperFormat: 'A4',
margin: { top: '5mm', bottom: '5mm', left: '5mm', right: '5mm' },
printBackground: true
},
{
// 发票打印配置
silent: true,
printerName: printerName,
copies: 1,
duplexMode: 'simplex'
}
);
}
async getInvoicePrinter() {
// 获取系统发票打印机
const printers = await webPrintPdf.utils.getPrinters();
return printers.find(p => p.name.includes('发票') || p.name.includes('Invoice'))?.name || 'default';
}
}
在Web开发中,传统的打印方案存在以下问题:
web-print-pdf npm包作为现代Web打印解决方案,巧妙地集成了SumatraPDF作为其核心打印引擎,解决了传统方案的所有问题:
// web-print-pdf npm包的核心架构
import webPrintPdf from 'web-print-pdf';
// 前端开发者只需要关注业务逻辑
const silentPrintExample = async () => {
try {
// web-print-pdf npm包内部自动调用SumatraPDF进行静默打印
const result = await webPrintPdf.printHtml(
'<h1>测试文档</h1><p>这是静默打印的内容</p>',
{
// PDF配置
paperFormat: 'A4',
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' },
printBackground: true
},
{
// 打印配置 - 这些参数会转换为SumatraPDF命令行参数
printerName: 'HP LaserJet Pro', // 转换为 -print-to 参数
copies: 2, // 转换为 2x 参数
duplexMode: 'duplex', // 转换为 duplex 参数
colorful: true // 转换为 color 参数
}
);
console.log('静默打印成功:', result);
return result;
} catch (error) {
console.error('静默打印失败:', error);
throw error;
}
};
// 打印性能监控
class PrintPerformanceMonitor {
constructor() {
this.metrics = {
totalTasks: 0,
successfulTasks: 0,
failedTasks: 0,
averageTime: 0,
totalTime: 0
};
}
recordTask(startTime, success, error = null) {
const duration = Date.now() - startTime;
this.metrics.totalTasks++;
this.metrics.totalTime += duration;
this.metrics.averageTime = this.metrics.totalTime / this.metrics.totalTasks;
if (success) {
this.metrics.successfulTasks++;
} else {
this.metrics.failedTasks++;
console.error('打印任务失败:', error);
}
// 记录详细日志
this.logTask(duration, success, error);
}
logTask(duration, success, error) {
const logEntry = {
timestamp: new Date().toISOString(),
duration,
success,
error: error?.message || null
};
console.log('打印任务记录:', logEntry);
}
getPerformanceReport() {
const successRate = (this.metrics.successfulTasks / this.metrics.totalTasks * 100).toFixed(2);
return {
...this.metrics,
successRate: `${successRate}%`,
averageTimeFormatted: `${this.metrics.averageTime.toFixed(2)}ms`
};
}
}
SumatraPDF + web-print-pdf npm包的组合为Web静默打印提供了完美的解决方案:
随着技术的不断发展,这种解决方案可以扩展到更多场景:
对于需要实现Web静默打印的开发者,web-print-pdf npm包是最佳选择:
SumatraPDF作为轻量级PDF引擎,为Web静默打印提供了强大的技术基础。而web-print-pdf npm包则将这些技术能力封装成开发者友好的API,让复杂的打印功能变得简单易用。选择web-print-pdf npm包,就是选择了一个成熟、可靠、易用的Web打印解决方案。
在Web打印技术的演进过程中,SumatraPDF和web-print-pdf npm包代表了开源技术与现代Web开发的完美结合,为开发者提供了实现静默打印功能的最佳实践。
相关技术:Node.js, Electron, Playwright, Puppeteer, Chrome DevTools Protocol, WebSocket, 无头浏览器, PDF生成, 打印驱动, 打印机管理, 打印队列, 批量打印, 打印预览, 打印监控, 打印日志, 打印性能优化
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。