依托HarmonyOS 6.1最新特性实现图片编辑APP(四):PixelMap图像变换实战

前言

在前三篇文章中,我们完成了 Image Kit 的架构理解和图片解码/编码的基础学习。从本文开始,我们将进入图片编辑的核心领域——PixelMap图像变换。图片编辑功能是用户最直观感知的能力,也是ImageEditor Pro APP的核心价值所在。

PixelMap(位图对象)是 Image Kit 中用于承载图片像素数据的核心对象,它提供了丰富的图像变换API,包括裁剪(Crop)、缩放(Scale)、平移(Translate)、旋转(Rotate)、翻转(Flip)和透明度调整(Opacity)等操作。

PixelMap是Image Kit中用于承载一张图片像素数据的位图对象,可读取或写入像素数据,并支持裁剪、缩放、旋转、镜像等图像处理操作。它是图片编辑的基础。

一、PixelMap图像变换能力概览

1.1 支持的变换操作

PixelMap 提供的图像变换API如下:

操作API方法说明参数
裁剪crop(region)裁剪指定区域x, y, width, height
缩放scale(x, y)缩放图片尺寸宽度缩放比例, 高度缩放比例
平移translate(x, y)平移图片位置水平偏移像素, 垂直偏移像素
旋转rotate(angle)旋转图片旋转角度(顺时针度数)
翻转flip(horizontal, vertical)水平/垂直翻转水平翻转, 垂直翻转
透明度opacity(alpha)设置全局透明度0.0(完全透明)到1.0(完全不透明)

1.2 变换操作的前提条件

在对 PixelMap 进行变换操作之前,需要确保满足以下条件:

  1. PixelMap 必须是通过解码选项 editable: true 创建的
  2. 操作执行前需要获取图片信息,确保操作参数在有效范围内
  3. 所有变换操作都是异步的,返回Promise
// 确保PixelMap可编辑
let decodingOptions: image.DecodingOptions = {
  editable: true,  // 必须设置为true
  desiredPixelFormat: image.PixelMapFormat.RGBA_8888
};

二、基础图像变换操作

2.1 图片裁剪(Crop)

图片裁剪是最常用的编辑操作之一,用于截取图片的指定区域:

import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 图片裁剪工具
class ImageCropUtil {
  // 基础裁剪:从左上角裁剪指定尺寸
  static async cropFromTopLeft(
    pixelMap: image.PixelMap,
    width: number,
    height: number
  ): Promise<void> {
    try {
      await pixelMap.crop({
        x: 0,
        y: 0,
        size: { width: width, height: height }
      });
      console.info(`Crop completed: ${width} x ${height}`);
    } catch (error) {
      console.error(`Crop failed: ${error}`);
    }
  }
  
  // 中心裁剪:裁剪图片中心区域
  static async cropCenter(
    pixelMap: image.PixelMap,
    targetWidth: number,
    targetHeight: number
  ): Promise<void> {
    try {
      let imageInfo = await pixelMap.getImageInfo();
      let x = Math.max(0, (imageInfo.size.width - targetWidth) / 2);
      let y = Math.max(0, (imageInfo.size.height - targetHeight) / 2);
      
      await pixelMap.crop({
        x: x,
        y: y,
        size: { width: targetWidth, height: targetHeight }
      });
      console.info(`Center crop completed: ${targetWidth} x ${targetHeight}`);
    } catch (error) {
      console.error(`Center crop failed: ${error}`);
    }
  }
  
  // 智能裁剪:根据宽高比裁剪
  static async cropToAspectRatio(
    pixelMap: image.PixelMap,
    aspectRatio: number // 宽/高,如 16/9 = 1.778
  ): Promise<void> {
    try {
      let imageInfo = await pixelMap.getImageInfo();
      let currentRatio = imageInfo.size.width / imageInfo.size.height;
      
      let cropWidth: number;
      let cropHeight: number;
      
      if (currentRatio > aspectRatio) {
        // 当前图片更宽,裁剪宽度
        cropHeight = imageInfo.size.height;
        cropWidth = Math.round(cropHeight * aspectRatio);
      } else {
        // 当前图片更高,裁剪高度
        cropWidth = imageInfo.size.width;
        cropHeight = Math.round(cropWidth / aspectRatio);
      }
      
      let x = Math.round((imageInfo.size.width - cropWidth) / 2);
      let y = Math.round((imageInfo.size.height - cropHeight) / 2);
      
      await pixelMap.crop({
        x: x,
        y: y,
        size: { width: cropWidth, height: cropHeight }
      });
      console.info(`Aspect ratio crop completed: ${cropWidth} x ${cropHeight}`);
    } catch (error) {
      console.error(`Aspect ratio crop failed: ${error}`);
    }
  }
}

2.2 图片缩放(Scale)

缩放操作可以改变图片的显示尺寸:

// 图片缩放工具
class ImageScaleUtil {
  // 按比例缩放
  static async scaleByRatio(
    pixelMap: image.PixelMap,
    ratioX: number,
    ratioY: number
  ): Promise<void> {
    try {
      await pixelMap.scale(ratioX, ratioY);
      console.info(`Scale completed: ${ratioX}x, ${ratioY}y`);
    } catch (error) {
      console.error(`Scale failed: ${error}`);
    }
  }
  
  // 缩放到指定尺寸
  static async scaleToSize(
    pixelMap: image.PixelMap,
    targetWidth: number,
    targetHeight: number
  ): Promise<void> {
    try {
      let imageInfo = await pixelMap.getImageInfo();
      let ratioX = targetWidth / imageInfo.size.width;
      let ratioY = targetHeight / imageInfo.size.height;
      
      await pixelMap.scale(ratioX, ratioY);
      console.info(`Scale to size completed: ${targetWidth} x ${targetHeight}`);
    } catch (error) {
      console.error(`Scale to size failed: ${error}`);
    }
  }
  
  // 等比例缩放(限制最大尺寸)
  static async scaleToMaxSize(
    pixelMap: image.PixelMap,
    maxSize: number
  ): Promise<void> {
    try {
      let imageInfo = await pixelMap.getImageInfo();
      let maxDimension = Math.max(imageInfo.size.width, imageInfo.size.height);
      let ratio = maxSize / maxDimension;
      
      if (ratio < 1.0) {
        await pixelMap.scale(ratio, ratio);
        console.info(`Scale to max size completed: ${maxSize}`);
      } else {
        console.info('Image is already smaller than max size, no scaling needed.');
      }
    } catch (error) {
      console.error(`Scale to max size failed: ${error}`);
    }
  }
}

2.3 图片旋转(Rotate)

旋转操作支持任意角度旋转:

// 图片旋转工具
class ImageRotateUtil {
  // 顺时针旋转90度
  static async rotate90(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.rotate(90);
      console.info('Rotated 90 degrees clockwise.');
    } catch (error) {
      console.error(`Rotate 90 failed: ${error}`);
    }
  }
  
  // 顺时针旋转180度
  static async rotate180(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.rotate(180);
      console.info('Rotated 180 degrees.');
    } catch (error) {
      console.error(`Rotate 180 failed: ${error}`);
    }
  }
  
  // 顺时针旋转270度
  static async rotate270(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.rotate(270);
      console.info('Rotated 270 degrees.');
    } catch (error) {
      console.error(`Rotate 270 failed: ${error}`);
    }
  }
  
  // 根据Exif旋转角度自动校正
  static async autoRotateByExif(
    pixelMap: image.PixelMap,
    exifOrientation: number
  ): Promise<void> {
    const rotationMap: Record<number, number> = {
      1: 0,    // 正常
      3: 180,  // 旋转180度
      6: 90,   // 旋转90度(顺时针)
      8: 270   // 旋转270度(顺时针)
    };
    
    const rotation = rotationMap[exifOrientation] || 0;
    if (rotation > 0) {
      await pixelMap.rotate(rotation);
      console.info(`Auto rotated by Exif orientation: ${rotation} degrees`);
    }
  }
}

2.4 图片翻转(Flip)

翻转操作支持水平和垂直两个方向:

// 图片翻转工具
class ImageFlipUtil {
  // 水平翻转(镜像)
  static async flipHorizontal(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.flip(true, false);
      console.info('Horizontal flip completed.');
    } catch (error) {
      console.error(`Horizontal flip failed: ${error}`);
    }
  }
  
  // 垂直翻转
  static async flipVertical(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.flip(false, true);
      console.info('Vertical flip completed.');
    } catch (error) {
      console.error(`Vertical flip failed: ${error}`);
    }
  }
  
  // 同时水平和垂直翻转(相当于旋转180度)
  static async flipBoth(pixelMap: image.PixelMap): Promise<void> {
    try {
      await pixelMap.flip(true, true);
      console.info('Both horizontal and vertical flip completed.');
    } catch (error) {
      console.error(`Both flip failed: ${error}`);
    }
  }
}

2.5 透明度调整(Opacity)

// 透明度调整工具
class ImageOpacityUtil {
  // 设置全局透明度
  static async setOpacity(pixelMap: image.PixelMap, alpha: number): Promise<void> {
    try {
      // alpha范围:0.0(完全透明)到1.0(完全不透明)
      const clampedAlpha = Math.max(0.0, Math.min(1.0, alpha));
      await pixelMap.opacity(clampedAlpha);
      console.info(`Opacity set to: ${clampedAlpha}`);
    } catch (error) {
      console.error(`Opacity adjustment failed: ${error}`);
    }
  }
  
  // 添加半透明水印效果
  static async applyWatermarkEffect(pixelMap: image.PixelMap, opacity: number = 0.3): Promise<void> {
    // 注意:这只是一个简单的透明度调整示例
    // 实际水印需要叠加另一个图片
    await this.setOpacity(pixelMap, opacity);
  }
}

三、组合变换操作

3.1 变换链式操作

在实际编辑场景中,经常需要组合多种变换操作。以下是ImageEditor Pro中的变换链处理:

// 变换操作记录
interface TransformOperation {
  type: 'crop' | 'scale' | 'rotate' | 'flip' | 'opacity' | 'translate';
  params: Record<string, number | boolean>;
  timestamp: number;
}

// 变换链管理器
class TransformChainManager {
  private operations: TransformOperation[] = [];
  private pixelMap: image.PixelMap;
  
  constructor(pixelMap: image.PixelMap) {
    this.pixelMap = pixelMap;
  }
  
  // 添加操作到变换链
  addOperation(type: string, params: Record<string, number | boolean>): void {
    this.operations.push({
      type: type as TransformOperation['type'],
      params: params,
      timestamp: Date.now()
    });
  }
  
  // 执行整个变换链
  async executeChain(): Promise<void> {
    for (const op of this.operations) {
      try {
        switch (op.type) {
          case 'crop':
            await this.pixelMap.crop({
              x: op.params.x as number,
              y: op.params.y as number,
              size: {
                width: op.params.width as number,
                height: op.params.height as number
              }
            });
            break;
          case 'scale':
            await this.pixelMap.scale(
              op.params.ratioX as number,
              op.params.ratioY as number
            );
            break;
          case 'rotate':
            await this.pixelMap.rotate(op.params.angle as number);
            break;
          case 'flip':
            await this.pixelMap.flip(
              op.params.horizontal as boolean,
              op.params.vertical as boolean
            );
            break;
          case 'opacity':
            await this.pixelMap.opacity(op.params.alpha as number);
            break;
          case 'translate':
            await this.pixelMap.translate(
              op.params.x as number,
              op.params.y as number
            );
            break;
        }
        console.info(`Executed ${op.type} successfully.`);
      } catch (error) {
        console.error(`Failed to execute ${op.type}: ${error}`);
      }
    }
  }
  
  // 获取操作历史
  getOperations(): TransformOperation[] {
    return [...this.operations];
  }
  
  // 撤销最后一个操作(需要从原始图片重新执行)
  // 注意:PixelMap的变换是不可逆的,需要从原始图片重新开始
  getOperationCount(): number {
    return this.operations.length;
  }
  
  // 清空操作链
  clear(): void {
    this.operations = [];
  }
}

3.2 常用组合操作模板

// 预设变换模板
class TransformPresets {
  // 正方形裁切(Instagram风格)
  static async squareCrop(pixelMap: image.PixelMap): Promise<void> {
    let imageInfo = await pixelMap.getImageInfo();
    let squareSize = Math.min(imageInfo.size.width, imageInfo.size.height);
    let x = Math.round((imageInfo.size.width - squareSize) / 2);
    let y = Math.round((imageInfo.size.height - squareSize) / 2);
    
    await pixelMap.crop({
      x: x,
      y: y,
      size: { width: squareSize, height: squareSize }
    });
    console.info('Square crop completed.');
  }
  
  // 16:9宽屏裁切
  static async wideScreenCrop(pixelMap: image.PixelMap): Promise<void> {
    let imageInfo = await pixelMap.getImageInfo();
    const targetRatio = 16 / 9;
    let cropHeight = imageInfo.size.height;
    let cropWidth = Math.round(cropHeight * targetRatio);
    
    if (cropWidth > imageInfo.size.width) {
      cropWidth = imageInfo.size.width;
      cropHeight = Math.round(cropWidth / targetRatio);
    }
    
    let x = Math.round((imageInfo.size.width - cropWidth) / 2);
    let y = Math.round((imageInfo.size.height - cropHeight) / 2);
    
    await pixelMap.crop({
      x: x,
      y: y,
      size: { width: cropWidth, height: cropHeight }
    });
    console.info('16:9 widescreen crop completed.');
  }
  
  // 4:3标准比例裁切
  static async standardCrop(pixelMap: image.PixelMap): Promise<void> {
    let imageInfo = await pixelMap.getImageInfo();
    const targetRatio = 4 / 3;
    let cropHeight = imageInfo.size.height;
    let cropWidth = Math.round(cropHeight * targetRatio);
    
    if (cropWidth > imageInfo.size.width) {
      cropWidth = imageInfo.size.width;
      cropHeight = Math.round(cropWidth / targetRatio);
    }
    
    let x = Math.round((imageInfo.size.width - cropWidth) / 2);
    let y = Math.round((imageInfo.size.height - cropHeight) / 2);
    
    await pixelMap.crop({
      x: x,
      y: y,
      size: { width: cropWidth, height: cropHeight }
    });
    console.info('4:3 standard crop completed.');
  }
  
  // 缩略图生成(缩放到200px以内)
  static async thumbnailTransform(pixelMap: image.PixelMap): Promise<void> {
    let imageInfo = await pixelMap.getImageInfo();
    let maxDimension = Math.max(imageInfo.size.width, imageInfo.size.height);
    let ratio = 200 / maxDimension;
    
    if (ratio < 1.0) {
      await pixelMap.scale(ratio, ratio);
    }
    console.info('Thumbnail transform completed.');
  }
}

四、图像信息获取

在对图片进行变换操作前后,获取图片信息是必要步骤:

// 图片信息获取工具
class ImageInfoUtil {
  // 获取完整的图片信息
  static async getFullImageInfo(pixelMap: image.PixelMap): Promise<ImageInfo> {
    try {
      let info = await pixelMap.getImageInfo();
      return {
        width: info.size.width,
        height: info.size.height,
        pixelFormat: info.pixelFormat,
        isHdr: info.isHdr,
        density: info.density
      };
    } catch (error) {
      console.error(`Failed to get image info: ${error}`);
      return {
        width: 0,
        height: 0,
        pixelFormat: 'Unknown',
        isHdr: false,
        density: 0
      };
    }
  }
  
  // 同步获取图片信息
  static getImageInfoSync(pixelMap: image.PixelMap): ImageInfo {
    try {
      let info = pixelMap.getImageInfoSync();
      return {
        width: info.size.width,
        height: info.size.height,
        pixelFormat: info.pixelFormat,
        isHdr: info.isHdr,
        density: info.density
      };
    } catch (error) {
      console.error(`Failed to get image info sync: ${error}`);
      return {
        width: 0,
        height: 0,
        pixelFormat: 'Unknown',
        isHdr: false,
        density: 0
      };
    }
  }
  
  // 获取图片尺寸信息
  static async getImageSize(pixelMap: image.PixelMap): Promise<{ width: number; height: number }> {
    let info = await pixelMap.getImageInfo();
    return {
      width: info.size.width,
      height: info.size.height
    };
  }
  
  // 获取图片字节数
  static async getImageBytes(pixelMap: image.PixelMap): Promise<number> {
    try {
      let info = await pixelMap.getImageInfo();
      let bytesPerPixel = 4; // RGBA_8888默认4字节
      return info.size.width * info.size.height * bytesPerPixel;
    } catch (error) {
      return 0;
    }
  }
}

interface ImageInfo {
  width: number;
  height: number;
  pixelFormat: string;
  isHdr: boolean;
  density: number;
}

五、ImageEditor Pro编辑页面实现

5.1 编辑工具栏

// 编辑类型枚举
enum EditAction {
  CROP = 'crop',
  ROTATE = 'rotate',
  FLIP_H = 'flip_horizontal',
  FLIP_V = 'flip_vertical',
  SCALE_UP = 'scale_up',
  SCALE_DOWN = 'scale_down',
  SQUARE = 'square',
  RESET = 'reset'
}

// 编辑页面主组件
@Entry
@Component
struct EditorPage {
  @State pixelMap: image.PixelMap | undefined = undefined;
  @State imageWidth: number = 0;
  @State imageHeight: number = 0;
  @State currentAction: string = '无';
  @State isProcessing: boolean = false;
  private transformChain: TransformChainManager | undefined;
  
  // 初始化PixelMap
  async initPixelMap(filePath: string): Promise<void> {
    try {
      const imageSource = image.createImageSource(filePath);
      let decodingOptions: image.DecodingOptions = {
        editable: true,
        desiredPixelFormat: image.PixelMapFormat.RGBA_8888
      };
      this.pixelMap = await imageSource.createPixelMap(decodingOptions);
      await imageSource.release();
      
      if (this.pixelMap) {
        this.transformChain = new TransformChainManager(this.pixelMap);
        let info = await this.pixelMap.getImageInfo();
        this.imageWidth = info.size.width;
        this.imageHeight = info.size.height;
      }
    } catch (error) {
      console.error(`Init PixelMap failed: ${error}`);
    }
  }
  
  // 执行编辑操作
  async executeAction(action: EditAction): Promise<void> {
    if (!this.pixelMap || this.isProcessing) {
      return;
    }
    
    this.isProcessing = true;
    this.currentAction = action;
    
    try {
      switch (action) {
        case EditAction.ROTATE:
          await this.pixelMap.rotate(90);
          break;
        case EditAction.FLIP_H:
          await this.pixelMap.flip(true, false);
          break;
        case EditAction.FLIP_V:
          await this.pixelMap.flip(false, true);
          break;
        case EditAction.SCALE_UP:
          await this.pixelMap.scale(1.2, 1.2);
          break;
        case EditAction.SCALE_DOWN:
          await this.pixelMap.scale(0.8, 0.8);
          break;
        case EditAction.SQUARE:
          await TransformPresets.squareCrop(this.pixelMap);
          break;
      }
      
      // 更新图片尺寸信息
      let info = await this.pixelMap.getImageInfo();
      this.imageWidth = info.size.width;
      this.imageHeight = info.size.height;
    } catch (error) {
      console.error(`Action ${action} failed: ${error}`);
    } finally {
      this.isProcessing = false;
    }
  }
  
  build() {
    Column() {
      // 图片信息显示
      Row() {
        Text(`尺寸: ${this.imageWidth} x ${this.imageHeight}`)
          .fontSize(12)
          .padding(5)
        Text(`操作: ${this.currentAction}`)
          .fontSize(12)
          .padding(5)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      
      // 图片预览区域
      if (this.pixelMap) {
        Image(this.pixelMap)
          .width('100%')
          .layoutWeight(1)
          .objectFit(ImageFit.Contain)
      }
      
      if (this.isProcessing) {
        LoadingProgress()
          .width(40)
          .height(40)
      }
      
      // 编辑工具栏
      Row() {
        Button('旋转')
          .onClick(() => this.executeAction(EditAction.ROTATE))
        Button('水平翻转')
          .onClick(() => this.executeAction(EditAction.FLIP_H))
        Button('垂直翻转')
          .onClick(() => this.executeAction(EditAction.FLIP_V))
        Button('放大')
          .onClick(() => this.executeAction(EditAction.SCALE_UP))
        Button('缩小')
          .onClick(() => this.executeAction(EditAction.SCALE_DOWN))
        Button('正方形')
          .onClick(() => this.executeAction(EditAction.SQUARE))
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(10)
    }
    .width('100%')
    .height('100%')
  }
}

六、变换操作的最佳实践

6.1 变换操作注意事项

注意事项说明建议
不可逆性PixelMap变换是原地修改,不可撤销操作前保留原始PixelMap副本
质量损失多次缩放会导致质量下降尽量避免多次缩放,一次性计算目标尺寸
内存占用变换操作需要临时内存操作大图时注意内存管理
异步执行所有变换操作都是异步的使用async/await或Promise链处理
参数范围裁剪区域不能超出图片边界操作前检查参数有效性

6.2 撤销/重做机制

// 撤销/重做管理器
class UndoRedoManager {
  private history: image.PixelMap[] = [];
  private currentIndex: number = -1;
  private maxHistory: number = 20; // 最多保存20步历史
  
  // 保存当前状态
  async saveState(pixelMap: image.PixelMap): Promise<void> {
    // 由于PixelMap的不可变性,实际项目中需要复制PixelMap
    // 这里简化为记录操作步骤
    this.currentIndex++;
    if (this.currentIndex < this.history.length) {
      this.history = this.history.slice(0, this.currentIndex);
    }
    this.history.push(pixelMap);
    
    // 限制历史记录数量
    if (this.history.length > this.maxHistory) {
      this.history.shift();
      this.currentIndex--;
    }
  }
  
  // 是否可撤销
  canUndo(): boolean {
    return this.currentIndex > 0;
  }
  
  // 是否可重做
  canRedo(): boolean {
    return this.currentIndex < this.history.length - 1;
  }
  
  // 撤销
  async undo(): Promise<image.PixelMap | undefined> {
    if (this.canUndo()) {
      this.currentIndex--;
      return this.history[this.currentIndex];
    }
    return undefined;
  }
  
  // 重做
  async redo(): Promise<image.PixelMap | undefined> {
    if (this.canRedo()) {
      this.currentIndex++;
      return this.history[this.currentIndex];
    }
    return undefined;
  }
}

总结

在这里插入图片描述

本文详细介绍了 HarmonyOS 6.1 Image KitPixelMap 的图像变换操作,包括裁剪缩放旋转翻转平移透明度调整,并展示了ImageEditor Pro APP中编辑工具栏的完整实现。

掌握这些基础变换操作后,你就能实现图片编辑应用中最常用的一批功能。下一篇文章,我们将深入探讨 PixelMap位图操作——像素级图像处理与自定义滤镜的实现。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐