1. 概述

Electron 的打印功能建立在 Chromium 浏览器引擎之上,通过多进程架构将网页内容转换为可打印格式,并最终通过操作系统的打印子系统输出到物理打印机或生成 PDF 文件。理解 Electron 的打印原理,需要穿透三个层次:Electron 自身的 API 封装层Chromium 的打印中间件层、以及操作系统原生的打印子系统层

本文将从 webContents.print()webContents.printToPDF() 的调用出发,逐层剖析数据流在 Windows、macOS 和 Linux 三大平台下的完整路径,并重点分析默认打印机配置在 silent 模式下不生效这一常见问题的技术根源。

在这里插入图片描述

2. Electron 打印 API 总览

Electron 在 WebContents 接口上提供了两个核心打印方法:

2.1 webContents.print([options], [callback])

该方法调用操作系统的原生打印对话框(或通过 silent: true 直接打印),将当前网页内容发送到物理打印机。关键选项包括:

  • silent: 是否跳过系统打印对话框直接打印
  • deviceName: 指定目标打印机名称
  • pageSize: 自定义页面尺寸(微米单位)
  • printBackground: 是否打印 CSS 背景
  • margins: 页边距配置
  • color: 是否彩色打印
  • dpi: 分辨率设置

2.2 webContents.printToPDF(options)

该方法使用 Chromium 内置的打印预览机制将网页渲染为 PDF 数据流,不经过操作系统打印子系统。返回一个 Promise<Buffer>,包含生成的 PDF 二进制数据。默认配置为:

{
  marginsType: 0,        // 默认边距
  printBackground: false,  // 不打印背景
  printSelectionOnly: false,
  landscape: false,      // 纵向
  pageSize: 'A4',        // 默认 A4
  scaleFactor: 100
}

关键区别print() 走的是系统打印路径(依赖 OS 打印子系统),而 printToPDF() 走的是Chromium 内部 PDF 生成路径(Blink → Skia/PDFium → Buffer)。

3. 从 Electron 调用到操作系统处理的完整链路

3.1 JavaScript API 层

当开发者在主进程中调用:

win.webContents.print({ silent: true, deviceName: 'My-Printer' })

该调用首先进入 Electron 的 TypeScript/JavaScript 封装层。WebContents 的实现在 lib/browser/api/web-contents.ts 中定义了 JavaScript 接口,随后通过 Node.js 的绑定机制将调用转发到 C++ 层。

3.2 Electron C++ 绑定层

打印功能的 C++ 实现位于 shell/browser/api/electron_api_web_contents.cc 中。该层负责:

  1. 解析 JavaScript 传递的 options 对象,转换为 Chromium 的 base::Value::Dict 格式
  2. 构建打印设置字典printing::PrintSettings),包括纸张大小、方向、边距等
  3. 调用 Chromium 的打印基础设施printing::PrintViewManagerprinting::PrintBackendService

源码路径示意

JS (lib/browser/api/web-contents.ts)
  ↓ Node.js binding
C++ (shell/browser/api/electron_api_web_contents.cc)
  ↓ Chromium IPC
Chromium Print Layer (components/printing/)

electron_api_web_contents.cc 中,关键代码逻辑大致如下:

// 伪代码,示意 Electron 如何处理打印选项
if (options.Get("mediaSize", &media_size)) {
  settings.Set(printing::kSettingMediaSize, std::move(media_size));
} else {
  // 注意:此处存在硬编码默认值的逻辑
  settings.Set(printing::kSettingMediaSize, 
               base::Value::Dict()
                   .Set(printing::kSettingMediaSizeHeightMicrons, 297000)
                   .Set(printing::kSettingMediaSizeWidthMicrons, 210000));
}

3.3 Chromium 打印架构

Chromium 的打印系统采用多进程架构,核心组件分布在不同进程中:

进程分工
进程 职责
Renderer Process 执行 Blink 渲染引擎,生成页面的可视化表示,处理 @media print CSS 规则
Browser Process 管理打印对话框、与操作系统打印 API 交互、调度打印作业
Utility Process (可选)执行耗时的打印后端查询操作,避免阻塞主线程
打印流程
  1. 内容准备阶段:Renderer Process 中的 Blink 引擎根据 @media print 规则重新计算样式和布局,生成打印专用的布局树
  2. 渲染阶段:Blink 的 Paint 阶段将布局树转换为绘制指令(Display List),然后通过 Skia 图形库栅格化为位图或矢量数据
  3. 序列化阶段
    • 对于 printToPDF():Chromium 使用内置的 PDF 生成器(基于 PDFium/Skia)直接输出 PDF 数据流
    • 对于 print():Chromium 将渲染结果转换为操作系统可接受的中间格式(如 Windows 的 EMF/XPS、macOS/Linux 的 PDF),通过 IPC 发送到 Browser Process
  4. 系统提交阶段:Browser Process 调用操作系统原生打印 API 提交打印作业

3.4 Blink 渲染与打印输出

Blink 处理打印任务时,会执行一个简化的渲染管线:

HTML + CSS → DOM Tree → Style Resolution → Render Tree → Layout (Print) 
  → Paint → Skia Recording → Platform Print Output
英文原文 中文翻译 说明
HTML + CSS HTML + CSS 输入的原始文档和样式
DOM Tree DOM 树 解析 HTML 后生成的文档对象模型
Style Resolution 样式解析 计算每个节点的最终 CSS 样式
Render Tree 渲染树 合并 DOM 和样式后生成的可视化树
Layout (Print) 布局(打印) 针对打印介质计算元素的几何位置
Paint 绘制 将渲染树转换为绘制指令
Skia Recording Skia 录制 通过 Skia 图形库记录绘制操作
Platform Print Output 平台打印输出 最终生成平台相关的打印数据(如 PDF、PostScript)

关键特性

  • Blink 在打印模式下会忽略 overflow: hidden 等屏幕显示优化规则
  • @page CSS 规则可以覆盖 JavaScript 中传递的 landscape 参数
  • page-break-before: always; 可用于强制分页

4. 各操作系统打印子系统详解

当 Chromium 的 Browser Process 将打印作业提交到操作系统后,三大平台的处理路径出现显著分化。

4.1 Windows 打印架构

Windows 的打印子系统是一个一级操作系统子系统,由打印后台处理程序(Print Spooler)和一组可替换的打印机驱动程序组成。

核心组件
组件 说明
spoolsv.exe 打印后台处理服务,运行在所有打印路径的中心
localspl.dll 本地打印提供程序,包含 EMF 打印处理器
Winspool.drv 客户端打印 API DLL
Spoolss.dll 后台处理程序路由器
两条主要打印路径

Windows 自 Vista 起支持两条并行的打印路径:

GDIXPS 都是 Windows 下的图形/打印技术,但属于不同时代的方案:
GDI(Graphics Device Interface):Windows 最传统的 2D 图形 API,从 Windows 1.0 时代就存在,主要用于画图、显示窗口、打印文档。
特点

  • 基于设备上下文(DC)位图/矢量指令
  • 打印时,应用程序通过 GDI 把内容画到打印机的设备上下文上
  • 驱动程序再把 GDI 指令转成打印机能懂的PCL/PostScript/位图

缺点:依赖打印机驱动,不同驱动渲染效果可能不一致;高 DPI 下容易模糊

XPS(XML Paper Specification):微软推出的基于 XML 的固定文档格式,类似 PDF,主要用于:作为打印管道中的中间文档格式,实现"所见即所得"的精确打印
特点

  • 打印时,应用先把内容生成 XPS 文件(本质是打包的 XML + 资源)
  • XPS 驱动直接把这份固定格式发给打印机,不经过驱动二次解释
  • 和 PDF 类似,保证不同设备上显示/打印效果完全一致

优点:免驱动渲染、矢量保真、支持透明/渐变等高级效果

GDI XPS
本质 图形绘制 API 固定文档格式(类似 PDF)
年代 1985 年至今 2006 年推出(Vista 时代)
打印路径 App → GDI → 打印机驱动 → 打印机 App → XPS 文档 → XPS 驱动 → 打印机
是否依赖驱动渲染 ✅ 是,驱动解释 GDI 指令 ❌ 否,文档格式即最终呈现
跨设备一致性 差(不同驱动效果不同) 好(固定格式,所见即所得)
现代支持 仍广泛支持,但逐渐老化 Windows 仍支持,但推广不及 PDF

GDI 是 Windows 老式的"画图+打印"接口;XPS 是微软试图用来替代 GDI 打印路径的、类似 PDF 的精确文档格式。

GDI 打印路径(经典路径)

Win32/GDI 应用
  → GDI (CreateDC → DrvEnableDriver → DrvEnablePDEV)
  → EMF 后台处理文件(Enhanced Metafile)
  → spoolsv.exe(后台处理程序)
  → EMF 打印处理器(localspl.dll)回放 EMF
  → GDI + 打印机图形 DLL 渲染
  → 设备数据流(EngWritePrinter / WritePrinter)
  → 端口监视器(TCPMON / WSDMON)
  → 端口驱动程序
  → 打印机设备

XPS 打印路径(现代路径)

WPF 应用 或 XPS Print API
  → XPS 后台处理文件(OPC/ZIP + XAML)
  → spoolsv.exe
  → XPSDrv 筛选器管道(XPSDrv Render Module / v4 Render)
  → 设备 PDL 或光栅
  → 端口
  → 打印机设备
跨路径转换

Windows 提供了两个转换模块,确保任意应用可以打印到任意驱动程序:

  • MXDC(Microsoft XPS Document Converter):将 GDI 输出转换为 XPS,用于 GDI 应用 → XPS 打印机
  • XGC(XPS-to-GDI Conversion):将 XPS 转换为 EMF,用于 XPS 应用 → GDI 打印机
GDI EMF
本质 Windows 的 2D 图形 API(函数调用) GDI 绘图指令的序列化文件格式
关系 调用 LineTo()TextOut() 等函数画图 把这些函数调用逐条记录下来,存成 .emf 文件
类比 画家现场作画 把作画过程录成视频,以后可以回放
驱动模型演进
驱动模型 打印路径 核心格式 中间环节
v3 (GDI-based) App → GDI → 驱动图形DLL → 打印机 GDI/EMF 驱动负责全部渲染转换
v4 App → XPS → 驱动 → 打印机 XPS 系统提供通用渲染,驱动只做轻量转换
IPP 收件箱 + PSA App → XPS/IPP → 网络协议 → 打印机 XPS / PDF / PWG Raster 经常无需本地驱动

驱动模型越新,打印路径越"短":v3 是应用画给驱动看、驱动再翻译;v4 是应用生成标准文档给驱动封包;IPP 是应用直接通过网络把标准文档发给打印机自己处理。
IPP 全称 Internet Printing Protocol(互联网打印协议),是一种基于 HTTP 的网络打印标准。

Electron 在 Windows 上的打印路径

当 Electron 在 Windows 上调用 webContents.print() 时:

  1. Chromium 的 Browser Process 通过 Winspool.drv 调用 Windows 打印 API
  2. 如果目标打印机使用 GDI 驱动,Chromium 通常生成 EMF 格式的后台处理数据
  3. 后台处理程序将 EMF 数据路由到对应打印队列
  4. EMF 打印处理器回放绘图指令,调用打印机驱动程序的图形 DLL 进行渲染
  5. 渲染后的设备特定数据通过端口监视器发送到打印机

4.2 macOS 打印架构

macOS 的打印架构采用分层设计,上层为应用框架,底层为 CUPS。

CUPS 全称 Common UNIX Printing System(通用 Unix 打印系统),是 Linux/macOS 上负责管理打印的后台服务。

命令 作用
lpstat -a 查看所有打印机是否接受任务
lpstat -p 查看打印机状态(空闲/禁用/拒绝)
lpstat -d 查看系统默认打印机
lpstat -o 查看当前打印队列中的任务
lpstat -s 汇总显示所有打印机及默认目标
lpstat -l 显示详细信息
lpinfo -v 查看系统可用的打印机连接方式(USB/网络等)
lpinfo -m 查看可用的驱动/PPD 列表
分层结构
应用层(AppKit / NSView / NSDocument)
  → Core Printing(C API,如 PMCreateSession, PMSessionBeginDocument)
  → CUPS(cupsd 守护进程)
  → 筛选器链
  → 后端(usb, ipp, socket)
  → 打印机设备
关键技术特性
  1. Quartz 2D / Core Graphics:macOS 的绘图基础使用基于 PDF 的成像模型。当应用打印时,内容首先被描述为 PDF 数据,这与 CUPS 的原生格式高度兼容
  2. CUPS 集成:自 OS X 10.2 起,macOS 使用 CUPS 作为打印后端,cupsd 守护进程监听 IPP 端口 631
  3. 筛选器链:CUPS 根据 mime.convs 和 PPD 文件计算从作业 MIME 类型到打印机格式的最低成本转换链。典型路径为:
    PDF → pdftopdf → pdftoraster → CUPS Raster → 设备筛选器 → 打印机
    
  4. AirPrint:macOS 原生支持 AirPrint(基于 IPP Everywhere),可实现无驱动打印
Electron 在 macOS 上的打印路径
  1. Chromium 的 Browser Process 调用 macOS 的 Core Printing API
  2. 系统通过 Quartz 将渲染内容转换为 PDF 表示
  3. CUPS 接收 PDF 作业,排队存储在 /var/spool/cups/
  4. CUPS 调度器根据打印机配置选择合适的筛选器链
  5. 筛选器将 PDF 转换为打印机原生语言(PostScript、PCL、ZPL 等)
  6. CUPS 后端通过 USB/网络将数据发送到打印机

4.3 Linux 打印架构

Linux 的打印子系统以 CUPS 为核心,架构与 macOS 底层类似,但更加开放和可定制。

核心组件
组件 说明
cupsd CUPS 调度守护进程,监听 IPP 端口 631
/etc/cups/printers.conf 打印机配置文件
/var/spool/cups/ 后台处理文件存储位置
cups-filters OpenPrinting 提供的筛选器和后端集合
Ghostscript / Poppler / MuPDF PDF/PostScript 渲染引擎
打印数据流
应用(Electron/Chromium)
  → CUPS 客户端库(libcups)
  → cupsd(通过 IPP 协议提交作业)
  → 作业入队(控制 c-file + 数据 d-file)
  → 筛选器链(根据 MIME 类型转换)
    例如:application/pdf → pdftopdf → pdftoraster → CUPS Raster → 设备筛选器
  → 可选端口监视器
  → 后端(usb, ipp, socket, lpd)
  → 打印机设备(固件 RIP 处理 PDL)
CUPS 2.x vs 3.x 架构变迁
特性 CUPS 2.x CUPS 3.x(新架构)
驱动模型 支持 PPD 文件和经典驱动 移除 PPD,仅支持驱动程序应用(Printer Applications)
标准格式 PDF 作为标准作业格式 全 IPP,仅支持无驱动打印
筛选器 cups-filters 2.x 内置 IPP 处理
兼容性 支持传统打印机 仅支持 IPP Everywhere / AirPrint / Mopria 认证打印机
Electron 在 Linux 上的打印路径
  1. Chromium 的 Browser Process 通过 CUPS 客户端库提交打印作业
  2. 作业以 PDF 格式进入 CUPS 队列
  3. CUPS 根据打印机配置(PPD 或 printer application)确定筛选器链
  4. 筛选器将 PDF 转换为打印机支持的格式(如 PCL、PostScript、ESC/POS 等)
  5. 后端模块通过 USB 或网络将数据发送到打印机

5. 跨平台差异对比

维度 Windows macOS Linux
打印后台处理 Print Spooler (spoolsv.exe) CUPS (cupsd) CUPS (cupsd)
后台处理文件位置 %SystemRoot%\System32\spool\PRINTERS /var/spool/cups/ /var/spool/cups/
核心图形 API GDI / XPS / Direct2D Quartz 2D (Core Graphics) Cairo / Skia(Chromium 内部)
标准作业格式 EMF(GDI 路径)/ XPS(XPS 路径) PDF(Quartz 原生) PDF(CUPS 标准)
驱动模型 v3 (GDI) / v4 / IPP Class Driver CUPS 筛选器 + PPD / AirPrint CUPS 筛选器 + PPD / Printer Applications
无驱动打印 IPP Everywhere / Mopria(Win 10/11) AirPrint / IPP Everywhere(原生) IPP Everywhere / Mopria
颜色管理 Windows Color System + ICC ColorSync + ICC CUPS color management + ICC
打印对话框 系统原生对话框 系统原生对话框 GTK/Qt 对话框或 CUPS 对话框
Chromium 交互层 Winspool.drv API Core Printing API CUPS API (libcups)
热敏/票据打印机支持 依赖驱动配置 依赖 CUPS 配置 依赖 CUPS 配置(最常见场景)

关键差异分析

  1. 后台处理格式差异

    • Windows GDI 路径使用 EMF(增强型图元文件),这是一种记录 GDI 绘图指令的封闭格式,需要回放和二次渲染
    • macOS 和 Linux 使用 PDF 作为中间格式,与 Chromium 的输出格式天然兼容,减少了格式转换损耗
  2. 渲染位置差异

    • Windows GDI 路径中,渲染可能发生在主机端(通过打印机图形 DLL)或设备端(固件 RIP)
    • macOS/Linux 的 CUPS 路径中,渲染通常通过 Ghostscript/Poppler 在主机端完成,生成 CUPS Raster 后发送到设备
  3. 驱动配置方式差异

    • Windows 打印机配置存储在注册表和驱动特定的 DEVMODE 结构中
    • macOS/Linux 的 CUPS 配置存储在 /etc/cups/printers.conf 和 PPD 文件中

6. 默认打印机配置与 silent 模式行为分析

6.1 问题现象

在使用 webContents.print({ silent: true }) 时,开发者经常遇到以下问题:

现象 1:纸张大小不匹配

  • 在 Windows 上将默认打印机设置为 Letter 尺寸,但 silent 打印输出 A4
  • 在 Linux 上配置 CUPS 使用 80mm 热敏纸,但 silent 打印输出 A4 裁剪版

现象 2:方向设置被忽略

  • 在打印机首选项中设置为横向(Landscape),但 silent 打印输出纵向(Portrait)

现象 3:自定义页边距/分辨率失效

  • 打印机驱动中配置的自定义页边距或 DPI 在 silent 模式下不生效

6.2 根本原因

原因一:Electron 硬编码默认纸张大小

Electron 的 webContents.print()silent: true 且未显式指定 pageSize 时,会硬编码 A4(210mm × 297mm)作为默认纸张大小,而不是查询打印机驱动程序的配置:

// electron_api_web_contents.cc 中的逻辑(简化)
if (options.Get("mediaSize", &media_size)) {
  settings.Set(printing::kSettingMediaSize, std::move(media_size));
} else {
  // 硬编码 A4 - 与 Chromium 原生行为不一致
  settings.Set(printing::kSettingMediaSize,
               base::Value::Dict()
                   .Set(printing::kSettingMediaSizeHeightMicrons, 297000)
                   .Set(printing::kSettingMediaSizeWidthMicrons, 210000)
                   ...);
}

这与 Chromium 的 window.print() 行为形成对比——后者会查询打印机驱动程序获取原生默认纸张大小

原因二:silent 模式跳过系统打印对话框

silent: true 时,Electron 直接构建打印设置字典并提交作业,跳过了操作系统打印对话框。而系统打印对话框通常会:

  • 读取打印机驱动的默认 DEVMODE(Windows)或 PPD 配置(CUPS)
  • 将驱动配置与应用程序请求合并
  • 提供用户确认的机会

跳过对话框意味着 Electron 必须自行承担"读取驱动默认配置"的责任,但当前实现中这部分逻辑缺失。

原因三:跨平台配置存储差异
  • Windows:打印机默认配置存储在注册表和驱动的 DEVMODE 结构中,Electron 未实现跨驱动的 DEVMODE 查询
  • macOS/Linux:CUPS 配置存储在 printers.conf 和 PPD 中,但 Electron 的 silent 路径未通过 CUPS API 读取这些配置
原因四:Chromium 打印设置与系统设置的隔离

Chromium 的打印系统设计上将"内容渲染设置"(纸张大小、边距、方向)与"设备配置"(驱动设置)分离。在标准 Chrome 中,用户通过打印预览 UI 手动调整设置,这些调整会覆盖驱动默认值。Electron 的 silent 模式移除了用户调整环节,但未能自动填充驱动默认值。

6.3 解决方案与最佳实践

方案一:显式指定所有打印参数

最可靠的方案是在调用 print() 时显式传递所有关键参数,不依赖系统默认值:

const options = {
  silent: true,
  deviceName: 'My-Thermal-Printer',
  pageSize: {
    width: 80000,   // 80mm in microns
    height: 297000  // 或根据内容动态计算
  },
  margins: {
    marginType: 'none'  // 或自定义边距
  },
  landscape: false,
  dpi: { horizontal: 203, vertical: 203 },
  printBackground: true
};

win.webContents.print(options, (success, failureReason) => {
  if (!success) console.log(failureReason);
});
方案二:使用 printToPDF + 系统打印命令

对于需要精确控制输出格式的场景,可以先生成 PDF,再通过系统命令打印:

// 1. 生成 PDF
const pdfData = await win.webContents.printToPDF({
  pageSize: 'A4',
  printBackground: true
});
fs.writeFileSync('/tmp/output.pdf', pdfData);

// 2. 使用系统命令打印(Linux/macOS)
const { exec } = require('child_process');
exec('lp -d My-Printer -o media=Custom.80x200mm /tmp/output.pdf');

// Windows 可使用 SumatraPDF 等命令行工具
// exec('SumatraPDF.exe -print-to "My-Printer" -silent output.pdf');
方案三:使用 silent: false 让用户确认

如果业务场景允许,使用非静默模式可以让用户通过系统打印对话框确认设置:

win.webContents.print({ silent: false }, (success, failureReason) => {
  // 用户可以在系统对话框中调整设置
});
方案四:查询系统默认配置后传入

在 Electron 28+ 版本中,可以使用 webContents.getPrintersAsync() 获取打印机列表及其默认配置,然后手动构建打印参数:

const printers = await win.webContents.getPrintersAsync();
const defaultPrinter = printers.find(p => p.isDefault);

// 根据打印机名称推断纸张大小,或维护一个配置映射表
const printerConfig = loadPrinterConfig(defaultPrinter.name);

win.webContents.print({
  silent: true,
  deviceName: defaultPrinter.name,
  pageSize: printerConfig.pageSize,
  landscape: printerConfig.landscape
});
方案五:使用 usePrinterDefaultPageSize(Electron 28+)

Electron 28 及以上版本引入了 usePrinterDefaultPageSize 选项。当设置为 true 时,Electron 会尝试使用打印机驱动的默认页面尺寸:

win.webContents.print({
  silent: true,
  usePrinterDefaultPageSize: true,  // 尝试使用驱动默认纸张
  deviceName: 'My-Printer'
});

注意:该选项的行为依赖于 Chromium 底层实现,在某些驱动程序上可能仍不生效。

7. 总结

Electron 的打印功能是一个跨平台封装层,其本质是将 Chromium 的网页渲染能力与操作系统的打印子系统桥接起来。理解其原理的关键在于把握三个层次:

  1. Electron API 层:提供 JavaScript 接口,负责参数解析和格式转换,但存在硬编码默认值的问题
  2. Chromium 中间件层:处理多进程渲染、打印预览、PDF 生成,将网页内容转换为平台无关的打印描述
  3. 操作系统子系统层
    • Windows:GDI/XPS 双路径 + Print Spooler + 驱动模型(v3/v4/IPP)
    • macOS:AppKit → Core Printing → CUPS,基于 PDF 的成像模型
    • Linux:CUPS 为核心,筛选器链处理格式转换,后端负责设备通信

关于默认打印机配置是否一定会按配置执行

答案是否定的。在 silent: true 模式下,Electron 的 webContents.print() 不会自动读取打印机驱动的默认配置,而是使用内部硬编码的默认值(主要是 A4 纸张大小)。这会导致:

  • 热敏/票据打印机输出格式错误
  • 区域设置(Letter vs A4)被忽略
  • 驱动中的方向、边距、分辨率设置失效

根本原因在于:Electron 在简化打印流程时,移除了 Chromium 原生的"查询驱动默认设置"逻辑,同时跳过了系统打印对话框(该对话框通常负责合并应用请求与驱动默认配置)。

最佳实践是:在调用 print()显式传递所有关键参数pageSizelandscapemarginsdpi 等),或采用 printToPDF() + 系统命令的间接打印方案,以确保输出结果的可预测性。

参考资源

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐