HarmonyOS开发中的图像打印小知识

举报
Jack20 发表于 2026/06/21 11:39:05 2026/06/21
【摘要】 HarmonyOS开发中的图像打印:@ohos.print、打印任务管理、打印预览、打印参数配置、打印适配核心要点:打印是移动端最容易被忽视却最实用的功能之一——办公审批要打印、照片冲印要打印、票据凭证要打印。本文从@ohos.print框架出发,深入打印任务管理、预览渲染、参数配置和多设备适配,帮你打通从屏幕到纸张的最后一公里。项目说明核心API@ohos.print (PrintTas...

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 打印流程

一次完整的打印流程包含以下步骤:

  1. 发现打印机:扫描局域网/USB/蓝牙,找到可用打印机
  2. 创建打印任务:指定打印机和文档数据
  3. 配置打印参数:纸张、方向、色彩、份数等
  4. 渲染预览:将内容渲染为打印预览
  5. 提交打印:确认后发送到打印机
  6. 监听状态:跟踪打印任务的完成情况

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、实时打印机发现、暂停状态

一句话总结:图像打印的关键是「预览先行、参数适配、分辨率达标」——在用户按下打印按钮之前,让他看到的就是最终效果,这才是好的打印体验。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。