# 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 工作线程**。
- 大缓冲区分配(整页画布合成)应尽量避免,直接按实际尺寸编码可显著降低内存与耗时。