HarmonyOS鸿蒙Next中压缩图片时如何保留EXIF信息
HarmonyOS鸿蒙Next中压缩图片时如何保留EXIF信息 使用Image Kit的ImagePacker类型的packToData方法时,由于packingOption可选属性needsPackProperties默认为false,压缩图片数据时会丢失EXIF信息。
方案一:显式设置 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中,通过Image Kit的ImagePacker进行图片压缩时,若需保留EXIF信息,需显式设置packingOption.needsPackProperties为true。该属性默认为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位置等信息。


