# oh_printerClient **Repository Path**: spacemit-openharmony/oh_printerClient ## Basic Information - **Project Name**: oh_printerClient - **Description**: 本项目是一个运行于 OpenHarmony 6.1 系统上的打印机客户端终端应用 Demo。它旨在演示如何通过 OH6.1 的通信接口与打印机进行连接、指令发送、状态查询及打印任务执行。该代码可作为二次开发的参考模板,也可直接用于基础功能的快速验证 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-05 - **Last Updated**: 2026-08-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # FilePreviewPrintDemo 基于 OpenHarmony / ArkTS 的文件预览与网络打印示例应用。支持图片、文档(TXT)、表格(CSV)的本地预览,并通过 IPP 协议将文件转换为 PWG Raster 后提交到网络打印机完成打印。 - 包名:`com.example.filepreviewprint` - Ability:`EntryAbility` - 目标设备:API 12 - 验证打印机:Pantum P2500 系列(`ipp://192.168.223.1:631/ipp/print`) ## 功能特性 - 按类别选择文件:图片 / 文档 / 表格 / 任意文件 - 内置文件预览(图片、文本、CSV 表格) - IPP `Print-Job`(操作码 `0x0002`)提交打印 - 客户端将文件渲染为 **PWG Raster(300 DPI,8-bit sgray_8)** 格式 - 打印作业状态查询(`Get-Job-Attributes`) ## 项目结构 ``` entry/src/main/ets/ ├── components/ # 各类文件预览组件 ├── pages/ # Index 主页、打印机管理页 ├── services/ # 文件预览、打印机发现、IPP 打印服务 ├── models/ # 类型定义 └── utils/ ├── IppProtocol.ets # IPP 协议构造/解析 ├── PwgRasterEncoder.ets # 核心:RGBA → PWG Raster 编码器 └── PwgRasterEncoder ... # TaskPool 工作线程转换 ``` ## 打印链路概览 ```mermaid flowchart LR A[选择文件] --> B[预览] B --> C[点击立即打印] C --> D[渲染为像素图 RGBA] D --> E[TaskPool 工作线程
RGBA→PWG Raster
300DPI 8-bit sgray_8] E --> F[IPP Print-Job 提交] F --> G[打印机处理] G --> H[job-state=9 已完成] ``` ## 构建与运行 ```powershell # 设置 Node 环境并构建 HAP $env:NODE_HOME='D:\deveco\DevEco Studio\tools\node' $env:PATH="$env:NODE_HOME;$env:PATH" & 'D:\deveco\DevEco Studio\tools\hvigor\bin\hvigorw.bat' -p entry -d default assembleHap # 安装并启动 hdc file send 'entry/build/default/outputs/default/entry-default-signed.hap' '/data/local/tmp/entry-default-signed.hap' hdc shell "bm install -p /data/local/tmp/entry-default-signed.hap" hdc shell "aa start -b com.example.filepreviewprint -a EntryAbility" ``` ## 验证结果 通过模拟点击对三类文件进行端到端测试,并使用 `Get-Job-Attributes` 直接向打印机确认最终作业状态: | 类型 | 文件 | Job ID | 结果 | |------|------|--------|------| | 文档 | 测试文档.txt | 84 | `job-state=9` 已完成 | | 表格 | 数据表格.csv | 85 | `job-state=9` 已完成 | | 图片 | 刘亦菲.jpg | 86 | `job-state=9` 已完成 | --- ## Q&A:问题原因与解决方案 本节记录调试打印链路过程中遇到的关键问题、根因分析与修复方案,供后续维护参考。 ### Q1. 打印作业提交后立即返回 `client-error-not-possible (0x0404)`,无法被打印机接受? **现象**:调用 IPP `Print-Job` 后,打印机直接以 `client-error-not-possible` 拒绝请求,作业根本没有进入队列。 **根因**:请求中携带了打印机无法接受的作业属性: - `compression`:打印机不支持该属性组合; - `print-quality`:代码以关键字字符串 `"normal"` 发送,但该属性在 IPP 中是**枚举整数**(3/4/5),类型不匹配; - `sides`:多余属性。 **解决方案**:将 `Print-Job` 的作业属性精简为打印机实测可接受的最小集合,与验证通过的 IPP 信封保持一致: ```typescript const builder = new IppBuilder(0x0201) .operation(IppOp.PRINT_JOB) .requestId(Date.now() & 0x7FFFFFFF) .operationDefaults({ printerUri: printer.uri, requestingUserName: 'root' }) .attrStr(IppTag.NAME_WITHOUT_LANG, 'job-name', jobName) .attrStr(IppTag.MIME_MEDIA_TYPE, 'document-format', docFormat); builder.group(IppTag.JOB_ATTRS); builder.attrStr(IppTag.KEYWORD, 'media', opts.media); builder.attrInt('copies', opts.copies); builder.attrStr(IppTag.KEYWORD, 'print-color-mode', opts.colorMode); ``` 移除 `compression`、`print-quality`、`sides` 三个属性后,作业即被正常接受。 --- ### Q2. 作业被接受后,很快变为 `state=7`(canceled),提示 `job-canceled-by-user`,但根本没有人取消? **现象**:作业提交成功(返回 jobId),状态先进入 `processing`,随后被取消,原因是 `job-canceled-by-user`。使用打印机自带的 `cupsfilter` 生成的参考 PWG 文件通过 curl 直接提交时,同样被取消——说明问题出在**光栅格式本身不被支持**。 **根因**:通过 `Get-Printer-Attributes` 查询打印机能力后发现,之前生成的光栅格式两项都不受支持: - `pwg-raster-document-resolution-supported` = **300×300 和 600×600**,**不支持 100 DPI**; - `pwg-raster-document-type-supported` = **black_8 / sgray_8(仅 8-bit)**,**不支持 1-bit**。 而当时的编码器输出的是 100 DPI + 1-bit,两项都不满足,因此打印机以「取消」的方式拒绝了这些无法处理的作业。 **解决方案**:改为按打印机支持的格式编码——**300 DPI,8-bit sgray_8**。Letter 纸张在 300 DPI 下为 2550×3300 像素。PWG Raster 页头关键字段: | 字段 | 值 | 说明 | |------|----|----| | HWResolution | 300 / 300 | 分辨率 | | cupsBitsPerColor | 8 | 每颜色 8 位 | | cupsBitsPerPixel | 8 | 每像素 8 位 | | cupsBytesPerLine | w | 8-bit 时等于宽度 | | cupsColorSpace | 18 | sGray | | 灰度值存储 | 0=黑,255=白 | 直接写单字节 | 同时页头字段采用**大端序**写入,且文件字段偏移 = CUPS 规范偏移 + 4(因为文件开头有 4 字节同步字 `RaS2`)。 --- ### Q3. 改为 300 DPI 8-bit 后,打印大图时应用直接崩溃/卡死,编码永远无法完成? **现象**:日志只打印到「开始转换」,之后再无「转换完成」输出;进程从 `ps` 列表中消失。最初怀疑是内存溢出(OOM)。 **根因**:故障日志显示是 `THREAD_BLOCK_6S`,即 **appfreeze / ANR(应用无响应)**,并非 OOM(堆仅约 9.7MB)。原因是 **840 万像素的灰度转换循环运行在 UI 主线程上**,阻塞主线程超过 6 秒,被系统看门狗强制杀死。代码中的 `await delay(0)` 无法可靠让出主线程。 此外,图片路径还先将缩放后的图合成到一张 2550×3300 的整页白底画布上,额外分配约 67MB(白底缓冲 + 整页 pixelMap),进一步加重了内存与耗时。 **解决方案**(两处修改): 1. **将重计算迁移到 TaskPool 工作线程**:把 RGBA→PWG Raster 的像素循环放入 `@Concurrent` 修饰的独立工作线程函数 `convertRgbaWorker`,主线程保持响应。该函数必须**自包含**——仅使用参数和局部变量,内联 PackBits 编码,几何常量(`HEADER_SIZE`、`DPI`)作为参数传入。 ```typescript private async offloadConvert(rgbaBuf: ArrayBuffer, w: number, h: number): Promise { // 在 TaskPool 工作线程执行重计算,避免主线程 THREAD_BLOCK_6S 卡死 const task = new taskpool.Task(convertRgbaWorker, rgbaBuf, w, h, HEADER_SIZE, DPI); const result = await taskpool.execute(task) as ArrayBuffer; return new Uint8Array(result); } ``` 2. **取消整页白底合成**:`encodeImage` 直接按缩放后图片自身尺寸编码(打印机接受比纸张窄的光栅),省去约 67MB 分配。 修复后,最重的照片(约 680 万像素)在工作线程约 28 秒完成编码,主线程不再卡死,作业顺利提交并到达 `job-state=9`(已完成)。 --- ### Q4. 如何独立确认作业是否真正「打印完成」,而不仅仅是「已提交」? 应用内只在提交后 2 秒查询一次状态,容易只看到 `processing`。可在设备上用 curl 构造 `Get-Job-Attributes` 请求直接向打印机查询最终状态: ```bash /bin/curl -s --data-binary @getjob.ipp \ -H 'Content-Type: application/ipp' \ http://192.168.223.1:631/ipp/print -o resp.bin # 解析响应中的 job-state 字节:9 = completed,7 = canceled,5 = processing ``` `job-state=9` 且 `job-state-reasons=none`、`time-at-completed` 非零,即表示作业真正打印完成。 --- ### 经验小结 - 打印机对于**无法处理的格式**(不支持的分辨率/位深)会以「作业被用户取消」的方式拒绝,而非报明确的格式错误,排查时容易误导。 - 提交前务必用 `Get-Printer-Attributes` 核对光栅格式与打印机能力,不要凭假设。 - ArkTS 中对数百万元素的循环放在 UI 主线程会触发 ANR;重计算应放到 **TaskPool 工作线程**。 - 大缓冲区分配(整页画布合成)应尽量避免,直接按实际尺寸编码可显著降低内存与耗时。