HarmonyOS鸿蒙Next中压缩图片时如何保留EXIF信息

HarmonyOS鸿蒙Next中压缩图片时如何保留EXIF信息 使用Image Kit的ImagePacker类型的packToData方法时,由于packingOption可选属性needsPackProperties默认为false,压缩图片数据时会丢失EXIF信息。

3 回复

方案一:显式设置 needsPackProperties 为 true

import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

async function compressImageWithEXIF(sourceImagePath: string, targetPath: string): Promise<void> {
  try {
    // 1. 创建 ImageSource
    let imageSource = image.createImageSource(sourceImagePath);
    // 2. 创建 PixelMap
    let pixelMap = await imageSource.createPixelMap();
    // 3. 创建 ImagePacker
    let imagePacker = image.createImagePacker();
    // 4. 设置打包选项 - 关键:设置 needsPackProperties 为 true
    let packingOption: image.PackingOption = {
      format: 'image/jpeg',  // 或 'image/png', 'image/webp'
      quality: 90,           // 图片质量 0-100
      needsPackProperties: true,  // ⭐ 保留 EXIF 信息
      desiredDynamicRange: image.PackingDynamicRange.AUTO
    };
    // 5. 打包压缩
    let packedData = await imagePacker.packing(pixelMap, packingOption);
    // 6. 保存到文件
    let file = fileIo.openSync(targetPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    fileIo.writeSync(file.fd, packedData);
    fileIo.closeSync(file);
    // 7. 释放资源
    pixelMap.release();
    imageSource.release();
    imagePacker.release();
    console.info('Image compressed successfully with EXIF preserved');
  } catch (err) {
    console.error(`Failed to compress image: ${err}`);
  }
}

方案二:读取并手动重写 EXIF 信息

如果需要更精细的控制,可以先读取 EXIF 信息,压缩后再写回:

import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

async function compressImageWithManualEXIF(sourceImagePath: string, targetPath: string): Promise<void> {
  try {
    // 1. 创建 ImageSource
    let imageSource = image.createImageSource(sourceImagePath);
    // 2. 读取原始 EXIF 信息
    let imageInfo = await imageSource.getImageInfo();
    let exifInfo: Record<string, string | number> = {};
    // 获取常用的 EXIF 属性
    const exifKeys = [
      'BitsPerSample',
      'Orientation',
      'ImageLength',
      'ImageWidth',
      'GPSLatitude',
      'GPSLongitude',
      'GPSLatitudeRef',
      'GPSLongitudeRef',
      'DateTimeOriginal',
      'ExposureTime',
      'FNumber',
      'ISO',
      'Make',
      'Model'
    ];
    for (let key of exifKeys) {
      try {
        let value = await imageSource.getImageProperty(key);
        if (value) {
          exifInfo[key] = value;
        }
      } catch (err) {
        // 某些属性可能不存在,跳过
      }
    }
    // 3. 创建 PixelMap
    let pixelMap = await imageSource.createPixelMap();
    // 4. 创建 ImagePacker 并压缩
    let imagePacker = image.createImagePacker();
    let packingOption: image.PackingOption = {
      format: 'image/jpeg',
      quality: 90,
      needsPackProperties: true  // 保留 EXIF
    };
    let packedData = await imagePacker.packing(pixelMap, packingOption);
    // 5. 保存压缩后的图片
    let file = fileIo.openSync(targetPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    fileIo.writeSync(file.fd, packedData);
    fileIo.closeSync(file);
    // 6. 验证 EXIF 信息是否保留
    let newImageSource = image.createImageSource(targetPath);
    for (let key in exifInfo) {
      try {
        let value = await newImageSource.getImageProperty(key);
        console.info(`EXIF ${key}: ${value}`);
      } catch (err) {
        console.error(`Failed to get EXIF ${key}: ${err}`);
      }
    }
    // 7. 释放资源
    pixelMap.release();
    imageSource.release();
    newImageSource.release();
    imagePacker.release();
    console.info('Image compressed with manual EXIF check');
  } catch (err) {
    console.error(`Failed to compress image: ${err}`);
  }
}

方案三:封装通用的图片压缩工具类

import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

export interface CompressOptions {
  quality?: number;           // 压缩质量 0-100,默认 90
  format?: string;            // 图片格式,默认 'image/jpeg'
  preserveEXIF?: boolean;     // 是否保留 EXIF,默认 true
  maxWidth?: number;          // 最大宽度
  maxHeight?: number;         // 最大高度
}

export class ImageCompressor {
  /**
   * 压缩图片并保留 EXIF 信息
   * @param sourcePath 源图片路径
   * @param targetPath 目标图片路径
   * @param options 压缩选项
   */
  public static async compress(
    sourcePath: string,
    targetPath: string,
    options?: CompressOptions
  ): Promise<boolean> {
    const defaultOptions: CompressOptions = {
      quality: 90,
      format: 'image/jpeg',
      preserveEXIF: true
    };
    const finalOptions = { ...defaultOptions, ...options };
    try {
      // 1. 创建 ImageSource
      let imageSource = image.createImageSource(sourcePath);
      // 2. 获取图片信息
      let imageInfo = await imageSource.getImageInfo();
      console.info(`Original size: ${imageInfo.size.width}x${imageInfo.size.height}`);
      // 3. 计算目标尺寸
      let targetWidth = imageInfo.size.width;
      let targetHeight = imageInfo.size.height;
      if (finalOptions.maxWidth && targetWidth > finalOptions.maxWidth) {
        targetHeight = Math.floor(targetHeight * finalOptions.maxWidth / targetWidth);
        targetWidth = finalOptions.maxWidth;
      }
      if (finalOptions.maxHeight && targetHeight > finalOptions.maxHeight) {
        targetWidth = Math.floor(targetWidth * finalOptions.maxHeight / targetHeight);
        targetHeight = finalOptions.maxHeight;
      }
      // 4. 创建 PixelMap(如果需要缩放)
      let decodingOptions: image.DecodingOptions = {
        desiredSize: {
          width: targetWidth,
          height: targetHeight
        },
        desiredPixelFormat: image.PixelMapFormat.RGBA_8888
      };
      let pixelMap = await imageSource.createPixelMap(decodingOptions);
      // 5. 设置打包选项
      let packingOption: image.PackingOption = {
        format: finalOptions.format!,
        quality: finalOptions.quality!,
        needsPackProperties: finalOptions.preserveEXIF!  // ⭐ 关键设置
      };
      // 6. 压缩图片
      let imagePacker = image.createImagePacker();
      let packedData = await imagePacker.packing(pixelMap, packingOption);
      // 7. 保存到文件
      let file = fileIo.openSync(targetPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
      fileIo.writeSync(file.fd, packedData);
      fileIo.closeSync(file);
      // 8. 输出压缩信息
      let originalSize = fileIo.statSync(sourcePath).size;
      let compressedSize = packedData.byteLength;
      let ratio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
      console.info(`Compression ratio: ${ratio}% (${originalSize} -> ${compressedSize} bytes)`);
      // 9. 释放资源
      pixelMap.release();
      imageSource.release();
      imagePacker.release();
      return true;
    } catch (err) {
      console.error(`Image compression failed: ${err}`);
      return false;
    }
  }

  /**
   * 读取图片的 EXIF 信息
   * @param imagePath 图片路径
   * @returns EXIF 信息对象
   */
  public static async getEXIFInfo(imagePath: string): Promise<Record<string, string>> {
    let exifInfo: Record<string, string> = {};
    try {
      let imageSource = image.createImageSource(imagePath);
      const exifKeys = [
        'Orientation',
        'DateTimeOriginal',
        'GPSLatitude',
        'GPSLongitude',
        'Make',
        'Model',
        'ExposureTime',
        'FNumber',
        'ISOSpeedRatings',
        'FocalLength'
      ];
      for (let key of exifKeys) {
        try {
          let value = await imageSource.getImageProperty(key);
          if (value) {
            exifInfo[key] = value;
          }
        } catch (err) {
          // 忽略不存在的属性
        }
      }
      imageSource.release();
    } catch (err) {
      console.error(`Failed to get EXIF info: ${err}`);
    }
    return exifInfo;
  }
}

方案四:在 UI 组件中使用示例

import { ImageCompressor, CompressOptions } from './ImageCompressor';
import { picker } from '@kit.CoreFileKit';

@Entry
@Component
struct ImageCompressPage {
  @State sourceImageUri: string = '';
  @State compressedImageUri: string = '';
  // 选择图片
  async selectImage() {
    try {
      let photoPicker = new picker.PhotoViewPicker();
      let selectResult = await photoPicker.select({
        MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      });
      if (selectResult.photoUris.length > 0) {
        this.sourceImageUri = selectResult.photoUris[0];
        console.info(`Selected image: ${this.sourceImageUri}`);
      }
    } catch (err) {
      console.error(`Failed to select image: ${err}`);
    }
  }
  // 压缩图片
  async compressImage() {
    if (!this.sourceImageUri) {
      return;
    }
    try {
      // 生成目标路径
      let context = getContext(this);
      let targetPath = context.filesDir + '/compressed_' + Date.now() + '.jpg';
      // 压缩选项
      let options: CompressOptions = {
        quality: 85,
        format: 'image/jpeg',
        preserveEXIF: true,  // ⭐ 保留 EXIF 信息
        maxWidth: 1920,
        maxHeight: 1920
      };
      // 执行压缩
      let success = await ImageCompressor.compress(
        this.sourceImageUri,
        targetPath,
        options
      );
      if (success) {
        this.compressedImageUri = 'file://' + targetPath;
        // 读取并显示 EXIF 信息
        let exifInfo = await ImageCompressor.getEXIFInfo(targetPath);
        console.info('EXIF Info:', JSON.stringify(exifInfo));
      }
    } catch (err) {
      console.error(`Failed to compress image: ${err}`);
    }
  }
  build() {
    Column() {
      Text('图片压缩(保留EXIF)')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })
      if (this.sourceImageUri) {
        Image(this.sourceImageUri)
          .width('80%')
          .height(200)
          .objectFit(ImageFit.Contain)
          .margin({ bottom: 10 })
        Text('原始图片')
          .fontSize(14)
          .margin({ bottom: 20 })
      }
      if (this.compressedImageUri) {
        Image(this.compressedImageUri)
          .width('80%')
          .height(200)
          .objectFit(ImageFit.Contain)
          .margin({ bottom: 10 })
        Text('压缩后图片(已保留EXIF)')
          .fontSize(14)
          .margin({ bottom: 20 })
      }
      Button('选择图片')
        .onClick(() => this.selectImage())
        .margin({ bottom: 10 })
      Button('压缩图片(保留EXIF)')
        .onClick(() => this.compressImage())
        .enabled(this.sourceImageUri !== '')
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

重要参数说明

PackingOption 接口

interface PackingOption { format: string; // 图片格式: ‘image/jpeg’ | ‘image/png’ | ‘image/webp’ quality: number; // 压缩质量: 0-100 needsPackProperties?: boolean; // ⭐ 是否保留属性信息(EXIF),默认 false desiredDynamicRange?: PackingDynamicRange; // 动态范围 }

常见 EXIF 属性

  • DateTimeOriginal: 拍摄时间
  • GPSLatitude / GPSLongitude: GPS 坐标
  • Orientation: 图片方向
  • Make / Model: 相机厂商和型号
  • ExposureTime: 曝光时间
  • FNumber: 光圈值
  • ISOSpeedRatings: ISO 感光度
  • FocalLength: 焦距

更多关于HarmonyOS鸿蒙Next中压缩图片时如何保留EXIF信息的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用图像处理模块的ImagePacker进行图片压缩时,通过设置PackOptions保留EXIF信息。具体操作是在打包选项中配置includeExif为true。示例代码:

import image from '@ohos.multimedia.image';

let packOptions = {
  format: "image/jpeg",
  quality: 80,
  includeExif: true
};
// 后续使用ImagePacker进行压缩操作

这样即可在输出图片中保持原始EXIF数据。

在HarmonyOS Next中,通过Image Kit的ImagePacker进行图片压缩时,若需保留EXIF信息,需显式设置packingOption.needsPackPropertiestrue。该属性默认为false,因此压缩时会丢弃EXIF数据。示例代码如下:

let packingOption: image.PackingOption = {
  format: "image/jpeg",
  quality: 80,
  needsPackProperties: true // 启用此选项以保留EXIF
};
let imagePacker = new image.ImagePacker();
imagePacker.packToData(sourceImage, packingOption);

通过此配置,压缩后的图片将完整保留原始EXIF元数据,包括拍摄参数、GPS位置等信息。

回到顶部