Harmony学习之图片处理与相机调用

Harmony学习之图片处理与相机调用

一、场景引入

小明正在开发一个社交应用,需要实现用户上传头像、拍照分享等功能。他发现图片处理和相机调用是移动应用开发中的核心能力,但面对HarmonyOS的多媒体API,他感到有些困惑。本篇文章将带领小明系统学习HarmonyOS 5 API 12+中的图片处理和相机调用技术。

二、核心概念

2.1 图片处理相关API

HarmonyOS提供了丰富的图片处理能力,主要包含以下模块:

  • @ohos.multimedia.image:核心图片处理模块,支持图片编解码、裁剪、缩放等操作
  • @kit.MediaLibraryKit:媒体库管理,用于访问相册和保存图片
  • @ohos.file.picker:文件选择器,用于从相册选择图片
  • @ohos.file.fs:文件系统操作,用于读写图片文件

2.2 相机调用相关API

相机开发主要依赖以下模块:

  • @ohos.media.camera:相机核心API,提供相机设备管理和拍照功能
  • @ohos.multimedia.mediaLibrary:媒体库访问,用于保存拍摄的照片
  • @ohos.abilityAccessCtrl:权限管理,申请相机和存储权限

三、关键实现

3.1 权限配置

在开发图片处理和相机功能前,需要在module.json5中配置必要的权限:

// module.json5
{
  "requestPermissions": [
    {
      "name": "ohos.permission.CAMERA",
      "reason": "用于拍照功能",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.WRITE_MEDIA",
      "reason": "用于保存图片到相册",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.READ_IMAGEVIDEO",
      "reason": "用于访问相册图片",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}

3.2 从相册选择图片

使用PhotoViewPicker可以方便地从相册选择图片:

// entry/src/main/ets/pages/ImagePickerPage.ets
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import image from '@ohos.multimedia.image';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct ImagePickerPage {
  @State selectedImage: string = '';
  @State pixelMap: image.PixelMap | null = null;

  // 从相册选择图片
  async selectImageFromAlbum() {
    try {
      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;

      const photoPicker = new picker.PhotoViewPicker();
      const photoSelectResult = await photoPicker.select(photoSelectOptions);
      
      if (photoSelectResult.photoUris.length > 0) {
        this.selectedImage = photoSelectResult.photoUris[0];
        await this.loadImageToPixelMap(this.selectedImage);
      }
    } catch (error) {
      promptAction.showToast({ message: '选择图片失败' });
    }
  }

  // 将URI转换为PixelMap用于显示
  async loadImageToPixelMap(uri: string) {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      this.pixelMap = await imageSource.createPixelMap();
      fs.closeSync(file);
    } catch (error) {
      promptAction.showToast({ message: '图片加载失败' });
    }
  }

  build() {
    Column() {
      Button('选择图片')
        .onClick(() => {
          this.selectImageFromAlbum();
        })
        .margin(20)

      if (this.pixelMap) {
        Image(this.pixelMap)
          .width(300)
          .height(300)
          .objectFit(ImageFit.Contain)
          .borderRadius(10)
      }
    }
    .width('100%')
    .height('100%')
  }
}

3.3 调用相机拍照

实现相机拍照功能需要申请权限并配置相机参数:

// entry/src/main/ets/pages/CameraPage.ets
import camera from '@ohos.media.camera';
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct CameraPage {
  @State capturedImage: string = '';
  private cameraKit: camera.CameraKit | null = null;
  private cameraDevice: camera.Camera | null = null;

  // 申请相机权限
  async requestCameraPermission() {
    const atManager = abilityAccessCtrl.createAtManager();
    const context = getContext(this);
    
    try {
      const result = await atManager.requestPermissionsFromUser(context, [
        'ohos.permission.CAMERA',
        'ohos.permission.WRITE_MEDIA'
      ]);
      
      if (result.authResults[0] === 0) {
        await this.openCamera();
      } else {
        promptAction.showToast({ message: '相机权限申请失败' });
      }
    } catch (error) {
      promptAction.showToast({ message: '权限申请异常' });
    }
  }

  // 打开相机
  async openCamera() {
    try {
      const context = getContext(this);
      this.cameraKit = camera.getCameraKit(context);
      
      const cameraIds = this.cameraKit.getCameraIds();
      if (cameraIds.length === 0) {
        promptAction.showToast({ message: '未找到可用相机' });
        return;
      }

      // 选择后置摄像头
      const backCameraId = cameraIds.find(id => {
        const cameraInfo = this.cameraKit!.getCameraInfo(id);
        return cameraInfo.getFacingType() === camera.CameraFacing.FACING_BACK;
      });

      if (!backCameraId) {
        promptAction.showToast({ message: '未找到后置摄像头' });
        return;
      }

      this.cameraDevice = await this.cameraKit.createCamera(backCameraId);
      promptAction.showToast({ message: '相机准备就绪' });
    } catch (error) {
      promptAction.showToast({ message: '相机打开失败' });
    }
  }

  // 拍照
  async takePhoto() {
    if (!this.cameraDevice) {
      promptAction.showToast({ message: '请先打开相机' });
      return;
    }

    try {
      const photoOutput = await this.cameraDevice.createPhotoOutput();
      await this.cameraDevice.startPreview();
      
      const photo = await photoOutput.capture();
      this.capturedImage = await this.savePhotoToGallery(photo);
      
      promptAction.showToast({ message: '拍照成功' });
    } catch (error) {
      promptAction.showToast({ message: '拍照失败' });
    }
  }

  // 保存照片到相册
  async savePhotoToGallery(photo: camera.Photo): Promise<string> {
    const context = getContext(this);
    const mediaLib = mediaLibrary.getMediaLibrary(context);
    
    const photoAsset = await mediaLib.createAsset(
      mediaLibrary.MediaType.IMAGE,
      'IMG_' + Date.now() + '.jpg'
    );
    
    const file = await mediaLib.openAsset(photoAsset, mediaLibrary.OpenMode.WRITE_ONLY);
    await file.write(photo.getData());
    await file.close();
    
    return photoAsset.uri;
  }

  build() {
    Column() {
      Button('打开相机')
        .onClick(() => {
          this.requestCameraPermission();
        })
        .margin(10)

      Button('拍照')
        .onClick(() => {
          this.takePhoto();
        })
        .margin(10)

      if (this.capturedImage) {
        Image(this.capturedImage)
          .width(300)
          .height(300)
          .objectFit(ImageFit.Contain)
          .borderRadius(10)
      }
    }
    .width('100%')
    .height('100%')
  }
}

3.4 图片压缩处理

在实际应用中,经常需要对图片进行压缩处理:

// entry/src/main/ets/utils/ImageUtils.ets
import image from '@ohos.multimedia.image';
import fs from '@ohos.file.fs';

export class ImageUtils {
  // 压缩图片
  static async compressImage(
    uri: string,
    maxWidth: number = 800,
    quality: number = 80
  ): Promise<string | null> {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      
      // 获取图片原始尺寸
      const imageInfo = await imageSource.getImageInfo();
      const { width, height } = imageInfo.size;
      
      // 计算缩放比例
      const scale = Math.min(maxWidth / width, 1);
      const targetWidth = Math.floor(width * scale);
      const targetHeight = Math.floor(height * scale);
      
      // 创建PixelMap并压缩
      const decodeOptions = {
        desiredSize: { width: targetWidth, height: targetHeight }
      };
      
      const pixelMap = await imageSource.createPixelMap(decodeOptions);
      const imagePacker = image.createImagePacker();
      
      // 设置压缩参数
      const packOptions = {
        format: 'image/jpeg',
        quality: quality
      };
      
      const imageData = await imagePacker.packing(pixelMap, packOptions);
      
      // 保存压缩后的图片
      const compressedPath = uri.replace('.jpg', '_compressed.jpg');
      const compressedFile = fs.openSync(
        compressedPath,
        fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY
      );
      
      await fs.write(compressedFile.fd, imageData);
      fs.closeSync(compressedFile.fd);
      
      // 释放资源
      pixelMap.release();
      imagePacker.release();
      fs.closeSync(file);
      
      return compressedPath;
    } catch (error) {
      console.error('图片压缩失败:', error);
      return null;
    }
  }
}

3.5 图片裁剪功能

实现图片裁剪功能:

// entry/src/main/ets/utils/ImageUtils.ets
import image from '@ohos.multimedia.image';
import fs from '@ohos.file.fs';

export class ImageUtils {
  // 裁剪图片
  static async cropImage(
    uri: string,
    x: number,
    y: number,
    width: number,
    height: number
  ): Promise<string | null> {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      const pixelMap = await imageSource.createPixelMap();
      
      // 执行裁剪
      const croppedPixelMap = await pixelMap.crop({
        x: x,
        y: y,
        width: width,
        height: height
      });
      
      // 保存裁剪后的图片
      const imagePacker = image.createImagePacker();
      const imageData = await imagePacker.packing(croppedPixelMap, {
        format: 'image/jpeg',
        quality: 90
      });
      
      const croppedPath = uri.replace('.jpg', '_cropped.jpg');
      const croppedFile = fs.openSync(
        croppedPath,
        fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY
      );
      
      await fs.write(croppedFile.fd, imageData);
      fs.closeSync(croppedFile.fd);
      
      // 释放资源
      croppedPixelMap.release();
      pixelMap.release();
      imagePacker.release();
      fs.closeSync(file);
      
      return croppedPath;
    } catch (error) {
      console.error('图片裁剪失败:', error);
      return null;
    }
  }
}

四、实战案例

4.1 头像上传功能

结合前面学到的知识,实现一个完整的头像上传功能:

// entry/src/main/ets/pages/ProfilePage.ets
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import image from '@ohos.multimedia.image';
import { ImageUtils } from '../utils/ImageUtils';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct ProfilePage {
  @State avatarUri: string = '';
  @State pixelMap: image.PixelMap | null = null;

  // 选择头像
  async selectAvatar() {
    try {
      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;

      const photoPicker = new picker.PhotoViewPicker();
      const photoSelectResult = await photoPicker.select(photoSelectOptions);
      
      if (photoSelectResult.photoUris.length > 0) {
        const originalUri = photoSelectResult.photoUris[0];
        
        // 压缩图片
        const compressedUri = await ImageUtils.compressImage(originalUri, 300, 80);
        
        if (compressedUri) {
          this.avatarUri = compressedUri;
          await this.loadImageToPixelMap(this.avatarUri);
          promptAction.showToast({ message: '头像设置成功' });
        }
      }
    } catch (error) {
      promptAction.showToast({ message: '选择头像失败' });
    }
  }

  // 加载图片到PixelMap
  async loadImageToPixelMap(uri: string) {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      this.pixelMap = await imageSource.createPixelMap();
      fs.closeSync(file);
    } catch (error) {
      promptAction.showToast({ message: '图片加载失败' });
    }
  }

  build() {
    Column() {
      // 头像显示区域
      Stack() {
        if (this.pixelMap) {
          Image(this.pixelMap)
            .width(120)
            .height(120)
            .borderRadius(60)
            .objectFit(ImageFit.Cover)
        } else {
          Image($r('app.media.default_avatar'))
            .width(120)
            .height(120)
            .borderRadius(60)
        }
        
        // 上传按钮
        Button('+')
          .width(40)
          .height(40)
          .borderRadius(20)
          .backgroundColor(Color.Blue)
          .fontColor(Color.White)
          .fontSize(20)
          .position({ x: '80%', y: '80%' })
          .onClick(() => {
            this.selectAvatar();
          })
      }
      .width(120)
      .height(120)
      .margin(30)

      Text('点击上传头像')
        .fontSize(16)
        .fontColor(Color.Gray)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

五、最佳实践

5.1 权限管理最佳实践

  • 按需申请:不要一次性申请所有权限,在需要使用时再申请
  • 权限拒绝处理:处理用户拒绝权限的情况,提供友好的提示和引导
  • 权限状态检查:在调用敏感API前检查权限状态
// 检查相机权限
async checkCameraPermission(): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  const context = getContext(this);
  
  try {
    const permissions = await atManager.checkAccessToken(
      context,
      ['ohos.permission.CAMERA']
    );
    
    return permissions[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (error) {
    return false;
  }
}

5.2 内存管理最佳实践

  • 及时释放资源:使用完PixelMap、ImageSource等资源后及时调用release()方法
  • 避免内存泄漏:不要在循环中创建大量图片对象
  • 大图处理:对大尺寸图片进行压缩后再处理

5.3 性能优化

  • 图片懒加载:使用LazyForEach加载列表中的图片
  • 图片缓存:对网络图片进行本地缓存,避免重复下载
  • 渐进式加载:先加载低质量图片,再加载高质量图片

六、总结与行动建议

通过本篇文章的学习,小明掌握了HarmonyOS中图片处理和相机调用的核心技能。建议在实际开发中:

  1. 遵循权限管理规范,确保应用合规性
  2. 注意资源释放,避免内存泄漏问题
  3. 优化图片处理流程,提升用户体验
  4. 参考官方文档,了解最新API变化

在实际项目中,可以结合业务需求,灵活运用这些技术,实现更丰富的图片处理功能。

posted @ 2025-12-23 23:19  J_____P  阅读(0)  评论(0)    收藏  举报