HarmonyOS开发中的图像打印小知识
HarmonyOS开发中的图像打印:@ohos.print、打印任务管理、打印预览、打印参数配置、打印适配
核心要点:打印是移动端最容易被忽视却最实用的功能之一——办公审批要打印、照片冲印要打印、票据凭证要打印。本文从@ohos.print框架出发,深入打印任务管理、预览渲染、参数配置和多设备适配,帮你打通从屏幕到纸张的最后一公里。
| 项目 | 说明 |
|---|---|
| 核心API | @ohos.print (PrintTask、PrintDocumentAdapter) |
一、背景与动机
你可能觉得,都2026年了,谁还打印啊?但现实是,打印需求无处不在:
- 办公场景:审批单、合同、报表,很多流程还是需要纸质版
- 生活场景:照片冲印、证件照、旅行纪念,实体照片有不可替代的温度
- 商业场景:小票打印、标签打印、快递单打印,零售和物流行业的刚需
HarmonyOS的打印框架@ohos.print提供了一套完整的打印解决方案,支持发现打印机、创建打印任务、配置打印参数、预览打印效果。但打印这个领域,坑特别多——不同打印机的能力差异、纸张尺寸的适配、色彩空间的转换、边距的处理……一个不小心,打出来的东西就歪了、糊了、裁切了。
今天咱们就把图像打印的完整链路从头到尾讲清楚,让你一次打对,不再浪费纸张。
二、核心原理
2.1 HarmonyOS打印框架架构
flowchart TD
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef info fill:#81C784,stroke:#388E3C,color:#000
classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
A[应用层] --> B[PrintDocumentAdapter]:::primary
B --> C[打印服务 PrintService]:::info
C --> D[打印机发现]:::warning
C --> E[打印任务管理]:::primary
C --> F[打印参数配置]:::purple
D --> D1[网络打印机]:::warning
D --> D2[USB打印机]:::warning
D --> D3[蓝牙打印机]:::warning
E --> E1[任务创建]:::primary
E --> E2[任务状态监听]:::primary
E --> E3[任务取消]:::error
F --> F1[纸张尺寸]:::purple
F --> F2[打印方向]:::purple
F --> F3[色彩模式]:::purple
F --> F4[份数与边距]:::purple
B --> G[打印预览渲染]:::info
G --> G1[PDF生成]:::info
G --> G2[缩放适配]:::info
2.2 打印流程
一次完整的打印流程包含以下步骤:
- 发现打印机:扫描局域网/USB/蓝牙,找到可用打印机
- 创建打印任务:指定打印机和文档数据
- 配置打印参数:纸张、方向、色彩、份数等
- 渲染预览:将内容渲染为打印预览
- 提交打印:确认后发送到打印机
- 监听状态:跟踪打印任务的完成情况
2.3 打印适配的关键概念
| 概念 | 说明 |
|---|---|
| PrintDocumentAdapter | 打印文档适配器,应用通过它向打印框架提供文档内容 |
| PrintAttributes | 打印属性,包含纸张尺寸、分辨率、边距等 |
| PrintJob | 打印任务,包含任务状态和打印参数 |
| PrintMargin | 打印边距,包含上下左右的边距值 |
| PrintResolution | 打印分辨率,水平和垂直DPI |
三、代码实战
3.1 打印机发现与打印任务管理
import { print } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 打印管理器
* 封装打印机发现、任务创建、状态监听等功能
*/
export class PrintManager {
// 打印任务实例
private printTask: print.PrintTask | null = null;
// 打印机列表
private printerList: print.PrinterInfo[] = [];
// 任务状态回调
private onTaskStateChange?: (state: print.PrintTaskState) => void;
/**
* 发现打印机
* 搜索局域网内可用的打印机
*/
async discoverPrinters(): Promise<print.PrinterInfo[]> {
try {
// 查询已连接的打印机
const printers = await print.queryPrinterInfoList();
this.printerList = printers;
console.info(`[PrintManager] 发现${printers.length}台打印机`);
for (const printer of printers) {
console.info(`[PrintManager] 打印机: ${printer.printerName}, 状态: ${printer.printerState}`);
}
return printers;
} catch (err) {
const error = err as BusinessError;
console.error(`[PrintManager] 发现打印机失败: ${error.code} - ${error.message}`);
return [];
}
}
/**
* 打印图片
* @param imageFd 图片文件描述符
* @param printOptions 打印选项
*/
async printImage(imageFd: number, printOptions?: PrintImageOptions): Promise<void> {
try {
// 构建打印文档适配器
const adapter = this.createImagePrintAdapter(imageFd, printOptions);
// 构建打印属性
const printAttributes: print.PrintAttributes = {
colorMode: printOptions?.colorMode ?? print.ColorMode.COLOR_MODE_MONOCHROME,
duplexMode: printOptions?.duplexMode ?? print.DuplexMode.DUPLEX_MODE_NONE,
pageSize: printOptions?.pageSize ?? this.getDefaultPageSize(),
minMargin: printOptions?.margin ?? this.getDefaultMargin()
};
// 创建打印任务
this.printTask = await print.print(adapter, printAttributes);
// 监听任务状态
this.setupTaskListener();
console.info('[PrintManager] 打印任务已创建');
} catch (err) {
const error = err as BusinessError;
console.error(`[PrintManager] 打印失败: ${error.code} - ${error.message}`);
}
}
/**
* 创建图片打印文档适配器
*/
private createImagePrintAdapter(imageFd: number, options?: PrintImageOptions): print.PrintDocumentAdapter {
const adapter: print.PrintDocumentAdapter = {
// 开始布局
onStartLayoutWrite: (job: print.PrintJob, writeResultCallback: (writeResult: print.PrintWriteResultCallback) => void) => {
console.info('[PrintAdapter] 开始布局写入');
// 构建PDF文件数据(实际项目中需要将图片转为PDF)
const writeResult: print.PrintWriteResultCallback = {
getFileFd: () => {
return imageFd;
}
};
writeResultCallback(writeResult);
},
// 任务完成
onJobStateChanged: (job: print.PrintJob, state: print.PrintJobState) => {
console.info(`[PrintAdapter] 任务状态变化: ${state}`);
}
};
return adapter;
}
/**
* 设置任务状态监听
*/
private setupTaskListener(): void {
if (this.printTask === null) return;
this.printTask.on('stateChange', (state: print.PrintTaskState) => {
console.info(`[PrintManager] 打印任务状态: ${state}`);
const stateMap: Record<number, string> = {
[print.PrintTaskState.PRINT_TASK_STATE_IDLE]: '空闲',
[print.PrintTaskState.PRINT_TASK_STATE_RUNNING]: '运行中',
[print.PrintTaskState.PRINT_TASK_STATE_BLOCKED]: '阻塞',
[print.PrintTaskState.PRINT_TASK_STATE_COMPLETED]: '已完成',
[print.PrintTaskState.PRINT_TASK_STATE_FAILED]: '失败',
[print.PrintTaskState.PRINT_TASK_STATE_CANCELLED]: '已取消'
};
console.info(`[PrintManager] 状态描述: ${stateMap[state] ?? '未知'}`);
if (this.onTaskStateChange) {
this.onTaskStateChange(state);
}
});
}
/**
* 取消打印任务
*/
cancelTask(): void {
// 注意:PrintTask目前没有直接的cancel方法
// 需要通过系统打印界面取消
console.info('[PrintManager] 请通过系统打印界面取消任务');
}
/**
* 获取默认纸张尺寸(A4)
*/
private getDefaultPageSize(): print.PrintPageSize {
return {
id: 'ISO_A4',
name: 'A4',
width: 210000, // 单位:1/1000毫米,210mm
height: 297000 // 297mm
};
}
/**
* 获取默认边距
*/
private getDefaultMargin(): print.PrintMargin {
return {
top: 5000, // 5mm
bottom: 5000,
left: 5000,
right: 5000
};
}
/**
* 设置状态变化回调
*/
setOnTaskStateChange(callback: (state: print.PrintTaskState) => void): void {
this.onTaskStateChange = callback;
}
}
/**
* 图片打印选项
*/
export interface PrintImageOptions {
// 色彩模式
colorMode?: print.ColorMode;
// 双面模式
duplexMode?: print.DuplexMode;
// 纸张尺寸
pageSize?: print.PrintPageSize;
// 打印边距
margin?: print.PrintMargin;
// 打印份数
copies?: number;
// 缩放模式
scaleMode?: 'fit' | 'fill' | 'original';
}
3.2 打印预览渲染
在提交打印之前,用户通常需要预览打印效果。打印预览的核心是将图片按打印参数渲染到一个预览区域。
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
/**
* 打印预览渲染器
* 将图片按打印参数渲染为预览PixelMap
*/
export class PrintPreviewRenderer {
// A4纸的宽高比(210:297)
private static readonly A4_RATIO: number = 210 / 297;
/**
* 渲染打印预览
* @param sourcePixelMap 原始图片
* @param pageSize 纸张尺寸
* @param margin 打印边距
* @param previewWidth 预览区域宽度
* @returns 预览PixelMap
*/
static async renderPreview(
sourcePixelMap: image.PixelMap,
pageSize: print.PrintPageSize,
margin: print.PrintMargin,
previewWidth: number
): Promise<image.PixelMap> {
// 计算预览高度(按纸张宽高比)
const paperWidthMM = pageSize.width / 1000; // 转为毫米
const paperHeightMM = pageSize.height / 1000;
const previewHeight = Math.round(previewWidth * (paperHeightMM / paperWidthMM));
// 计算可打印区域(扣除边距)
const marginTopMM = margin.top / 1000;
const marginBottomMM = margin.bottom / 1000;
const marginLeftMM = margin.left / 1000;
const marginRightMM = margin.right / 1000;
const printableWidthMM = paperWidthMM - marginLeftMM - marginRightMM;
const printableHeightMM = paperHeightMM - marginTopMM - marginBottomMM;
// 计算预览中的可打印区域(像素)
const scale = previewWidth / paperWidthMM;
const printableWidthPx = Math.round(printableWidthMM * scale);
const printableHeightPx = Math.round(printableHeightMM * scale);
const marginLeftPx = Math.round(marginLeftMM * scale);
const marginTopPx = Math.round(marginTopMM * scale);
// 创建预览画布
const previewOptions: image.PixelMapInitializationOptions = {
editable: true,
pixelFormat: image.PixelMapFormat.RGBA_8888,
size: { width: previewWidth, height: previewHeight }
};
const previewPixelMap = await image.createPixelMap(0xFFFFFFFF, previewOptions);
// 获取源图片信息
const sourceInfo = sourcePixelMap.getImageInfo();
const sourceWidth = sourceInfo.size.width;
const sourceHeight = sourceInfo.size.height;
// 计算图片在可打印区域中的缩放和位置(居中适配)
const sourceRatio = sourceWidth / sourceHeight;
const printableRatio = printableWidthPx / printableHeightPx;
let drawWidth: number;
let drawHeight: number;
if (sourceRatio > printableRatio) {
// 图片更宽,以宽度为准
drawWidth = printableWidthPx;
drawHeight = Math.round(printableWidthPx / sourceRatio);
} else {
// 图片更高,以高度为准
drawHeight = printableHeightPx;
drawWidth = Math.round(printableHeightPx * sourceRatio);
}
// 居中偏移
const offsetX = marginLeftPx + Math.round((printableWidthPx - drawWidth) / 2);
const offsetY = marginTopPx + Math.round((printableHeightPx - drawHeight) / 2);
// 读取源图片像素数据
const sourceBuffer = new ArrayBuffer(sourceWidth * sourceHeight * 4);
sourcePixelMap.readPixelsToBuffer(sourceBuffer);
// 读取预览画布像素数据
const previewBuffer = new ArrayBuffer(previewWidth * previewHeight * 4);
previewPixelMap.readPixelsToBuffer(previewBuffer);
const sourceView = new DataView(sourceBuffer);
const previewView = new DataView(previewBuffer);
// 将源图片像素缩放写入预览画布
for (let dy = 0; dy < drawHeight; dy++) {
for (let dx = 0; dx < drawWidth; dx++) {
// 计算源图片中对应的采样坐标
const sx = Math.round(dx * sourceWidth / drawWidth);
const sy = Math.round(dy * sourceHeight / drawHeight);
// 读取源像素
const srcOffset = (sy * sourceWidth + sx) * 4;
const r = sourceView.getUint8(srcOffset);
const g = sourceView.getUint8(srcOffset + 1);
const b = sourceView.getUint8(srcOffset + 2);
const a = sourceView.getUint8(srcOffset + 3);
// 写入预览像素
const px = offsetX + dx;
const py = offsetY + dy;
if (px >= 0 && px < previewWidth && py >= 0 && py < previewHeight) {
const dstOffset = (py * previewWidth + px) * 4;
previewView.setUint8(dstOffset, r);
previewView.setUint8(dstOffset + 1, g);
previewView.setUint8(dstOffset + 2, b);
previewView.setUint8(dstOffset + 3, a);
}
}
}
// 写回预览PixelMap
previewPixelMap.writeBufferToPixels(previewBuffer);
console.info(`[PrintPreview] 预览渲染完成: ${previewWidth}x${previewHeight}`);
return previewPixelMap;
}
/**
* 绘制纸张边框和边距线
* 在预览图上叠加边框辅助线
*/
static drawPreviewGuides(
previewPixelMap: image.PixelMap,
pageSize: print.PrintPageSize,
margin: print.PrintMargin,
previewWidth: number
): void {
// 纸张边框用灰色虚线表示
// 边距线用浅蓝色虚线表示
// 实际实现需要Canvas绘制,这里仅说明原理
console.info('[PrintPreview] 绘制预览辅助线');
}
}
3.3 完整的图像打印UI
将打印管理、预览渲染、参数配置整合到一个完整的UI中。
import { print } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
@Entry
@Component
struct ImagePrintDemo {
// 打印管理器
private printManager: PrintManager = new PrintManager();
// 原始图片
@State sourcePixelMap: image.PixelMap | undefined = undefined;
// 预览图片
@State previewPixelMap: image.PixelMap | undefined = undefined;
// 打印状态
@State printStatus: string = '未开始';
// 打印机列表
@State printers: string[] = [];
// 打印参数
@State selectedPageSize: number = 0; // 0=A4, 1=A3, 2=Letter
@State colorMode: number = 0; // 0=彩色, 1=黑白
@State copies: number = 1;
@State marginTop: number = 5; // 毫米
@State marginBottom: number = 5;
@State marginLeft: number = 5;
@State marginRight: number = 5;
@State orientation: number = 0; // 0=纵向, 1=横向
// 纸张尺寸选项
private pageSizeOptions: Array<{ name: string; width: number; height: number }> = [
{ name: 'A4 (210×297mm)', width: 210000, height: 297000 },
{ name: 'A3 (297×420mm)', width: 297000, height: 420000 },
{ name: 'Letter (216×279mm)', width: 216000, height: 279000 },
{ name: '4×6英寸 (102×152mm)', width: 102000, height: 152000 },
{ name: '5×7英寸 (127×178mm)', width: 127000, height: 178000 }
];
aboutToAppear(): void {
this.printManager.setOnTaskStateChange((state: print.PrintTaskState) => {
const stateNames: Record<number, string> = {
[print.PrintTaskState.PRINT_TASK_STATE_IDLE]: '空闲',
[print.PrintTaskState.PRINT_TASK_STATE_RUNNING]: '打印中...',
[print.PrintTaskState.PRINT_TASK_STATE_BLOCKED]: '阻塞',
[print.PrintTaskState.PRINT_TASK_STATE_COMPLETED]: '打印完成',
[print.PrintTaskState.PRINT_TASK_STATE_FAILED]: '打印失败',
[print.PrintTaskState.PRINT_TASK_STATE_CANCELLED]: '已取消'
};
this.printStatus = stateNames[state] ?? '未知状态';
});
}
/**
* 加载图片
*/
async loadImage(): Promise<void> {
try {
const photoPicker = new picker.PhotoViewPicker();
const selectResult = await photoPicker.select({
maxSelectNumber: 1,
MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE
});
if (selectResult.length > 0) {
const filePath = selectResult[0];
const imageSource = image.createImageSource(filePath);
this.sourcePixelMap = await imageSource.createPixelMap({
editable: false,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
imageSource.release();
// 自动生成预览
this.updatePreview();
}
} catch (err) {
console.error('[PrintDemo] 加载图片失败: ' + err);
}
}
/**
* 更新打印预览
*/
async updatePreview(): Promise<void> {
if (this.sourcePixelMap === undefined) return;
const pageSize = this.getCurrentPageSize();
const margin: print.PrintMargin = {
top: this.marginTop * 1000,
bottom: this.marginBottom * 1000,
left: this.marginLeft * 1000,
right: this.marginRight * 1000
};
try {
this.previewPixelMap = await PrintPreviewRenderer.renderPreview(
this.sourcePixelMap,
pageSize,
margin,
300
);
} catch (err) {
console.error('[PrintDemo] 预览渲染失败: ' + err);
}
}
/**
* 获取当前纸张尺寸
*/
getCurrentPageSize(): print.PrintPageSize {
const option = this.pageSizeOptions[this.selectedPageSize];
// 如果是横向,宽高互换
if (this.orientation === 1) {
return {
id: option.name,
name: option.name + ' (横向)',
width: option.height,
height: option.width
};
}
return {
id: option.name,
name: option.name,
width: option.width,
height: option.height
};
}
/**
* 执行打印
*/
async doPrint(): Promise<void> {
if (this.sourcePixelMap === undefined) {
console.warn('[PrintDemo] 请先加载图片');
return;
}
try {
this.printStatus = '准备打印...';
// 发现打印机
const printers = await this.printManager.discoverPrinters();
this.printers = printers.map(p => p.printerName);
if (printers.length === 0) {
this.printStatus = '未找到打印机';
return;
}
// 将PixelMap保存为临时文件
const tempPath = getContext(this).tempDir + '/print_temp.png';
const imagePackerApi = image.createImagePacker();
const packOpts: image.PackingOption = {
format: 'image/png',
quality: 100
};
const packData = await imagePackerApi.packing(this.sourcePixelMap, packOpts);
const file = fs.openSync(tempPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
fs.writeSync(file.fd, packData);
fs.closeSync(file.fd);
imagePackerApi.release();
// 打开文件描述符
const fd = fs.openSync(tempPath, fs.OpenMode.READ_ONLY).fd;
// 执行打印
const printOptions: PrintImageOptions = {
colorMode: this.colorMode === 0 ? print.ColorMode.COLOR_MODE_COLOR : print.ColorMode.COLOR_MODE_MONOCHROME,
pageSize: this.getCurrentPageSize(),
margin: {
top: this.marginTop * 1000,
bottom: this.marginBottom * 1000,
left: this.marginLeft * 1000,
right: this.marginRight * 1000
},
copies: this.copies
};
await this.printManager.printImage(fd, printOptions);
this.printStatus = '打印任务已提交';
} catch (err) {
this.printStatus = '打印失败';
console.error('[PrintDemo] 打印失败: ' + err);
}
}
build() {
Scroll() {
Column() {
// 标题
Text('图像打印')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 图片预览区域
Row() {
// 原图
Column() {
Text('原图')
.fontSize(14)
.fontColor('#AAAAAA')
.margin({ bottom: 4 })
if (this.sourcePixelMap) {
Image(this.sourcePixelMap)
.width(150)
.height(150)
.objectFit(ImageFit.Contain)
.borderRadius(4)
} else {
Column() {
Text('选择图片')
.fontSize(14)
.fontColor('#4FC3F7')
}
.width(150)
.height(150)
.justifyContent(FlexAlign.Center)
.backgroundColor('#1A1A2E')
.borderRadius(4)
.onClick(() => this.loadImage())
}
}
// 打印预览
Column() {
Text('打印预览')
.fontSize(14)
.fontColor('#AAAAAA')
.margin({ bottom: 4 })
if (this.previewPixelMap) {
Image(this.previewPixelMap)
.width(150)
.height(Math.round(150 * 297 / 210))
.objectFit(ImageFit.Contain)
.borderRadius(4)
.border({ width: 1, color: '#333333' })
} else {
Column() {
Text('暂无预览')
.fontSize(14)
.fontColor('#666666')
}
.width(150)
.height(Math.round(150 * 297 / 210))
.justifyContent(FlexAlign.Center)
.backgroundColor('#1A1A2E')
.borderRadius(4)
}
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ bottom: 20 })
// 打印状态
Row() {
Text('状态: ')
.fontSize(14)
.fontColor('#AAAAAA')
Text(this.printStatus)
.fontSize(14)
.fontColor('#4FC3F7')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ bottom: 16 })
// 打印参数配置
Column() {
Text('打印参数')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.margin({ bottom: 12 })
// 纸张尺寸
Row() {
Text('纸张尺寸')
.fontSize(14)
.fontColor('#AAAAAA')
.width(80)
Column() {
ForEach(this.pageSizeOptions, (option: { name: string }, index: number) => {
Row() {
Radio({ value: `page_${index}`, group: 'pageSize' })
.checked(index === this.selectedPageSize)
.onChange((isChecked: boolean) => {
if (isChecked) {
this.selectedPageSize = index;
this.updatePreview();
}
})
Text(option.name)
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 8 })
}
.margin({ bottom: 4 })
})
}
.layoutWeight(1)
}
.width('100%')
.margin({ bottom: 12 })
// 打印方向
Row() {
Text('打印方向')
.fontSize(14)
.fontColor('#AAAAAA')
.width(80)
Row() {
Radio({ value: 'portrait', group: 'orientation' })
.checked(this.orientation === 0)
.onChange((isChecked: boolean) => {
if (isChecked) { this.orientation = 0; this.updatePreview(); }
})
Text('纵向')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 4, right: 16 })
Radio({ value: 'landscape', group: 'orientation' })
.checked(this.orientation === 1)
.onChange((isChecked: boolean) => {
if (isChecked) { this.orientation = 1; this.updatePreview(); }
})
Text('横向')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 4 })
}
}
.width('100%')
.margin({ bottom: 12 })
// 色彩模式
Row() {
Text('色彩模式')
.fontSize(14)
.fontColor('#AAAAAA')
.width(80)
Row() {
Radio({ value: 'color', group: 'colorMode' })
.checked(this.colorMode === 0)
.onChange((isChecked: boolean) => {
if (isChecked) { this.colorMode = 0; }
})
Text('彩色')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 4, right: 16 })
Radio({ value: 'mono', group: 'colorMode' })
.checked(this.colorMode === 1)
.onChange((isChecked: boolean) => {
if (isChecked) { this.colorMode = 1; }
})
Text('黑白')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 4 })
}
}
.width('100%')
.margin({ bottom: 12 })
// 打印份数
Row() {
Text('打印份数')
.fontSize(14)
.fontColor('#AAAAAA')
.width(80)
Row() {
Button('-')
.width(32)
.height(32)
.fontSize(16)
.backgroundColor('#333333')
.fontColor('#FFFFFF')
.onClick(() => {
this.copies = Math.max(1, this.copies - 1);
})
Text(`${this.copies}`)
.width(40)
.textAlign(TextAlign.Center)
.fontSize(16)
.fontColor('#FFFFFF')
Button('+')
.width(32)
.height(32)
.fontSize(16)
.backgroundColor('#333333')
.fontColor('#FFFFFF')
.onClick(() => {
this.copies = Math.min(99, this.copies + 1);
})
}
}
.width('100%')
.margin({ bottom: 12 })
// 边距设置
Row() {
Text('边距(mm)')
.fontSize(14)
.fontColor('#AAAAAA')
.width(80)
Column() {
Row() {
Text('上:')
.fontSize(12)
.fontColor('#999999')
Slider({ value: this.marginTop, min: 0, max: 25, step: 1 })
.width(120)
.onChange((v: number) => {
this.marginTop = v;
this.updatePreview();
})
.selectedColor('#4FC3F7')
Text(`${this.marginTop}`)
.fontSize(12)
.fontColor('#FFFFFF')
.width(24)
}
Row() {
Text('下:')
.fontSize(12)
.fontColor('#999999')
Slider({ value: this.marginBottom, min: 0, max: 25, step: 1 })
.width(120)
.onChange((v: number) => {
this.marginBottom = v;
this.updatePreview();
})
.selectedColor('#4FC3F7')
Text(`${this.marginBottom}`)
.fontSize(12)
.fontColor('#FFFFFF')
.width(24)
}
Row() {
Text('左:')
.fontSize(12)
.fontColor('#999999')
Slider({ value: this.marginLeft, min: 0, max: 25, step: 1 })
.width(120)
.onChange((v: number) => {
this.marginLeft = v;
this.updatePreview();
})
.selectedColor('#4FC3F7')
Text(`${this.marginLeft}`)
.fontSize(12)
.fontColor('#FFFFFF')
.width(24)
}
Row() {
Text('右:')
.fontSize(12)
.fontColor('#999999')
Slider({ value: this.marginRight, min: 0, max: 25, step: 1 })
.width(120)
.onChange((v: number) => {
this.marginRight = v;
this.updatePreview();
})
.selectedColor('#4FC3F7')
Text(`${this.marginRight}`)
.fontSize(12)
.fontColor('#FFFFFF')
.width(24)
}
}
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor('#1A1A2E')
.borderRadius(12)
.margin({ bottom: 16 })
// 操作按钮
Row() {
Button('选择图片')
.backgroundColor('#4FC3F7')
.fontColor('#000000')
.onClick(() => this.loadImage())
Button('更新预览')
.backgroundColor('#CE93D8')
.fontColor('#000000')
.onClick(() => this.updatePreview())
Button('开始打印')
.backgroundColor('#81C784')
.fontColor('#000000')
.onClick(() => this.doPrint())
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#0D0D1A')
}
}
3.4 多设备打印适配
不同类型的打印机有不同的能力和限制,需要做针对性适配。
import { print } from '@kit.BasicServicesKit';
/**
* 打印适配器
* 针对不同打印机类型做适配处理
*/
export class PrintAdapter {
/**
* 根据打印机能力自动适配打印参数
* @param printerInfo 打印机信息
* @param desiredOptions 期望的打印选项
* @returns 适配后的打印选项
*/
static adaptPrintOptions(
printerInfo: print.PrinterInfo,
desiredOptions: PrintImageOptions
): PrintImageOptions {
const adaptedOptions: PrintImageOptions = { ...desiredOptions };
// 1. 色彩模式适配
// 如果打印机只支持黑白,强制使用黑白模式
if (printerInfo.capability?.colorModes !== undefined) {
const supportedColorModes = printerInfo.capability.colorModes;
if (desiredOptions.colorMode === print.ColorMode.COLOR_MODE_COLOR &&
!supportedColorModes.includes(print.ColorMode.COLOR_MODE_COLOR)) {
console.warn('[PrintAdapter] 打印机不支持彩色,切换为黑白');
adaptedOptions.colorMode = print.ColorMode.COLOR_MODE_MONOCHROME;
}
}
// 2. 纸张尺寸适配
// 如果打印机不支持期望的纸张尺寸,选择最接近的
if (printerInfo.capability?.pageSizes !== undefined) {
const supportedSizes = printerInfo.capability.pageSizes;
if (desiredOptions.pageSize && !supportedSizes.some(s => s.id === desiredOptions.pageSize?.id)) {
// 选择最接近的纸张
const closestSize = PrintAdapter.findClosestPageSize(supportedSizes, desiredOptions.pageSize);
if (closestSize) {
console.warn(`[PrintAdapter] 纸张不支持,切换为: ${closestSize.name}`);
adaptedOptions.pageSize = closestSize;
}
}
}
// 3. 边距适配
// 确保边距不小于打印机的最小边距
if (printerInfo.capability?.minMargin !== undefined) {
const minMargin = printerInfo.capability.minMargin;
if (adaptedOptions.margin) {
adaptedOptions.margin.top = Math.max(adaptedOptions.margin.top, minMargin.top);
adaptedOptions.margin.bottom = Math.max(adaptedOptions.margin.bottom, minMargin.bottom);
adaptedOptions.margin.left = Math.max(adaptedOptions.margin.left, minMargin.left);
adaptedOptions.margin.right = Math.max(adaptedOptions.margin.right, minMargin.right);
}
}
// 4. 份数适配
// 限制最大打印份数
if (adaptedOptions.copies && adaptedOptions.copies > 99) {
adaptedOptions.copies = 99;
}
return adaptedOptions;
}
/**
* 查找最接近的纸张尺寸
*/
private static findClosestPageSize(
supportedSizes: print.PrintPageSize[],
desiredSize: print.PrintPageSize
): print.PrintPageSize | null {
if (supportedSizes.length === 0) return null;
let closestSize = supportedSizes[0];
let minDiff = Infinity;
for (const size of supportedSizes) {
// 计算面积差异
const desiredArea = desiredSize.width * desiredSize.height;
const sizeArea = size.width * size.height;
const diff = Math.abs(desiredArea - sizeArea);
if (diff < minDiff) {
minDiff = diff;
closestSize = size;
}
}
return closestSize;
}
/**
* 生成打印适配报告
*/
static generateAdaptReport(
printerInfo: print.PrinterInfo,
originalOptions: PrintImageOptions,
adaptedOptions: PrintImageOptions
): string {
const changes: string[] = [];
if (originalOptions.colorMode !== adaptedOptions.colorMode) {
changes.push(`色彩模式: ${originalOptions.colorMode} → ${adaptedOptions.colorMode}`);
}
if (originalOptions.pageSize?.id !== adaptedOptions.pageSize?.id) {
changes.push(`纸张: ${originalOptions.pageSize?.name} → ${adaptedOptions.pageSize?.name}`);
}
if (originalOptions.copies !== adaptedOptions.copies) {
changes.push(`份数: ${originalOptions.copies} → ${adaptedOptions.copies}`);
}
if (changes.length === 0) {
return '打印参数无需适配,使用原始设置';
}
return `适配报告 (${printerInfo.printerName}):\n${changes.join('\n')}`;
}
}
四、踩坑与注意事项
4.1 打印权限声明
使用打印功能需要在module.json5中声明权限:
{
"requestPermissions": [
{
"name": "ohos.permission.PRINT",
"reason": "$string:print_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
如果不声明权限,调用打印API时会直接抛出权限拒绝错误。
4.2 PrintDocumentAdapter的回调时序
PrintDocumentAdapter的回调有严格的时序要求:
onStartLayoutWrite必须同步调用writeResultCallback- 如果回调时序不对,打印任务会直接失败,且错误信息不明确
建议:在onStartLayoutWrite中尽量做轻量操作,耗时操作(如文件读写)应在调用回调之前完成。
4.3 图片分辨率与打印质量
打印的分辨率远高于屏幕显示。一张在手机上看起来清晰的图片,打印出来可能很糊。
经验值:
- 6寸照片(4×6英寸):至少1200×1800像素
- A4纸全幅面:至少2480×3508像素(300DPI)
- 如果原图分辨率不够,不要强行放大,否则会模糊
建议:打印前检查图片分辨率,低于300DPI时给用户提示。
4.4 色彩空间差异
屏幕使用sRGB色彩空间,打印机通常使用CMYK色彩空间。RGB到CMYK的转换会导致颜色偏差——特别是鲜艳的蓝色和绿色,打印出来会变暗。
解决方案:
- 如果对色彩要求高,使用支持ICC色彩配置的打印机
- 打印预览中模拟CMYK效果,让用户提前感知色差
- 避免使用过于鲜艳的颜色
4.5 不同打印机的Disposal差异
不同品牌、型号的打印机对打印指令的支持程度不同:
- 某些打印机不支持自定义纸张尺寸
- 某些打印机不支持无边距打印
- 某些打印机不支持双面打印
建议:打印前先查询打印机能力(printerInfo.capability),根据能力动态调整打印参数。
4.6 打印任务的生命周期
打印任务创建后,即使应用退到后台或被销毁,任务仍在打印队列中。但任务状态的回调依赖于应用进程存活。
建议:
- 在
aboutToDisappear中取消任务监听 - 应用恢复前台时重新查询打印任务状态
- 重要打印操作建议持久化记录,防止丢失
五、HarmonyOS 6适配
5.1 打印API变化
| 变化项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| print方法 | print(adapter, attributes) |
不变,但新增printWithPreview方法 |
| PrintTask状态 | 6种状态 | 新增PRINT_TASK_STATE_PAUSED暂停状态 |
| 打印机发现 | queryPrinterInfoList() |
新增startPrinterDiscovery()/stopPrinterDiscovery()实时发现 |
| 打印预览 | 需手动实现 | 新增PrintPreviewAdapter系统级预览支持 |
| PDF打印 | 需手动转换 | 新增printPdf(filePath, attributes)直接打印PDF |
5.2 新增功能
// HarmonyOS 6新增:实时打印机发现
print.startPrinterDiscovery((printers: print.PrinterInfo[]) => {
console.info(`发现打印机: ${printers.length}台`);
// 实时更新打印机列表
});
// HarmonyOS 6新增:带预览的打印
const previewAdapter: print.PrintPreviewAdapter = {
onGetPreview: (attributes: print.PrintAttributes) => {
// 返回预览PixelMap
return previewPixelMap;
}
};
await print.printWithPreview(adapter, previewAdapter, printAttributes);
// HarmonyOS 6新增:直接打印PDF
await print.printPdf('/data/storage/el2/base/files/document.pdf', {
colorMode: print.ColorMode.COLOR_MODE_COLOR,
pageSize: a4Size,
copies: 2
});
5.3 迁移建议
- 原来手动实现的打印预览,可以迁移到
PrintPreviewAdapter - 原来手动转PDF再打印的流程,可以直接用
printPdf - 打印机发现从一次性查询改为实时监听,UI体验更好
六、总结
| 知识点 | 核心内容 |
|---|---|
| @ohos.print | 打印框架核心API,包含PrintDocumentAdapter、PrintTask、PrintAttributes |
| 打印任务管理 | 创建任务→配置参数→提交打印→监听状态;6种任务状态需要逐一处理 |
| 打印预览 | 按纸张宽高比渲染预览,计算可打印区域(扣除边距),图片居中适配 |
| 打印参数 | 纸张尺寸、打印方向、色彩模式、边距、份数;注意单位是1/1000毫米 |
| 打印适配 | 查询打印机能力,适配色彩/纸张/边距;生成适配报告告知用户变更 |
| 权限声明 | 必须声明ohos.permission.PRINT权限 |
| 分辨率要求 | 打印至少300DPI,6寸照片需1200×1800像素,A4需2480×3508像素 |
| 色彩差异 | 屏幕RGB vs 打印CMYK,鲜艳色会变暗;建议预览中模拟CMYK |
| HarmonyOS 6 | 新增printWithPreview、printPdf、实时打印机发现、暂停状态 |
一句话总结:图像打印的关键是「预览先行、参数适配、分辨率达标」——在用户按下打印按钮之前,让他看到的就是最终效果,这才是好的打印体验。
- 点赞
- 收藏
- 关注作者
评论(0)