411 lines
14 KiB
Plaintext
411 lines
14 KiB
Plaintext
import { image } from '@kit.ImageKit';
|
||
import { fileIo as fs } from '@kit.CoreFileKit';
|
||
import { common } from '@kit.AbilityKit';
|
||
import { BusinessError } from '@ohos.base';
|
||
import { util } from '@kit.ArkTS';
|
||
import { PickerUtil } from '@pura/picker_utils';
|
||
import componentSnapshot from '@ohos.arkui.componentSnapshot';
|
||
import { promptAction } from '@kit.ArkUI';
|
||
import { photoAccessHelper } from '@kit.MediaLibraryKit';
|
||
import { subjectSegmentation } from '@kit.CoreVisionKit';
|
||
import { StickerItem } from '../viewmodel/LocalBean';
|
||
import buffer from '@ohos.buffer';
|
||
import fileIo from '@ohos.file.fs';
|
||
import { ToastUtils } from '../dialog/ToastUtils';
|
||
|
||
|
||
export class ImageUtils {
|
||
private static readonly TAG: string = 'ImageUtils';
|
||
|
||
/**
|
||
* 压缩图片并保存到沙箱缓存目录
|
||
* @param context 上下文
|
||
* @param uri 原始图片URI (如 datashare:// 或 file://)
|
||
* @param quality 压缩质量 (0-100),默认 80
|
||
* @returns 压缩后的沙箱文件路径
|
||
*/
|
||
static async compressImage(context: common.Context, uri: string, quality: number = 80): Promise<string> {
|
||
let file: fs.File | undefined;
|
||
try {
|
||
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||
const packingOptions: image.PackingOption = {
|
||
format: 'image/jpeg',
|
||
quality: quality
|
||
};
|
||
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||
const arrayBuffer: ArrayBuffer = await imagePacker.packing(imageSource, packingOptions);
|
||
const fileName = `compress_${Date.now()}.jpg`;
|
||
const cachePath = `${context.cacheDir}/${fileName}`;
|
||
const cacheFile = fs.openSync(cachePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||
fs.writeSync(cacheFile.fd, arrayBuffer);
|
||
fs.closeSync(cacheFile);
|
||
|
||
await imagePacker.release();
|
||
await imageSource.release();
|
||
|
||
console.info(ImageUtils.TAG, `图片压缩成功: ${cachePath}`);
|
||
return `file://${cachePath}`; // 返回符合上传要求的 file 协议路径
|
||
|
||
} catch (error) {
|
||
const err = error as BusinessError;
|
||
console.error(ImageUtils.TAG, `压缩失败: ${err.code} ${err.message}`);
|
||
return uri; // 如果压缩失败,返回原路径尝试直接上传
|
||
} finally {
|
||
if (file) {
|
||
fs.closeSync(file);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将图片 URI 转换为 Base64 字符串
|
||
* @param uri 相册选择的 datashare:// 或沙箱路径
|
||
*/
|
||
static async base64Image(uri: string): Promise<string> {
|
||
const context = getContext();
|
||
// 使用临时路径存储拷贝的文件
|
||
const tempFileName = `${Date.now()}.jpg`;
|
||
const destPath = `${context.cacheDir}/${tempFileName}`;
|
||
|
||
let srcFile: fs.File | null = null;
|
||
try {
|
||
srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||
fs.copyFileSync(srcFile.fd, destPath);
|
||
const base64Data = await ImageUtils.fileToBase64(destPath);
|
||
fs.unlinkSync(destPath);
|
||
|
||
return base64Data;
|
||
} catch (err) {
|
||
const error = err as BusinessError;
|
||
console.error(`base64Image 转换失败: ${error.message}`);
|
||
return "";
|
||
} finally {
|
||
if (srcFile) {
|
||
fs.closeSync(srcFile);
|
||
}
|
||
}
|
||
}
|
||
|
||
static async pixelMapToBase64(pixelMap: image.PixelMap): Promise<string> {
|
||
const imagePackerApi = image.createImagePacker();
|
||
const packOptions: image.PackingOption = { format: 'image/png', quality: 100 };
|
||
const data = await imagePackerApi.packing(pixelMap, packOptions);
|
||
|
||
let base64Str = buffer.from(data).toString('base64');
|
||
return `data:image/png;base64,${base64Str}`;
|
||
}
|
||
|
||
/**
|
||
* 内部方法:读取文件转 Base64
|
||
*/
|
||
private static async fileToBase64(filePath: string): Promise<string> {
|
||
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
|
||
const stat = fs.statSync(file.fd);
|
||
const buffer = new ArrayBuffer(stat.size);
|
||
fs.readSync(file.fd, buffer);
|
||
fs.closeSync(file);
|
||
|
||
const helper = new util.Base64Helper();
|
||
return helper.encodeToStringSync(new Uint8Array(buffer));
|
||
}
|
||
|
||
|
||
/**
|
||
* 保存图片到指定路径(用户自己选择)
|
||
* @param pixelMap
|
||
*/
|
||
static async savePixelMapToGallery(pixelMap: image.PixelMap) {
|
||
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||
const packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
|
||
|
||
try {
|
||
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.jpg`]);
|
||
|
||
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||
|
||
if (!targetUri) {
|
||
console.warn('用户取消保存或路径获取失败');
|
||
return;
|
||
}
|
||
|
||
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||
fs.writeSync(file.fd, buffer);
|
||
fs.closeSync(file.fd);
|
||
|
||
console.info('保存成功!路径: ' + targetUri);
|
||
} catch (err) {
|
||
console.error('保存过程中出错: ' + JSON.stringify(err));
|
||
} finally {
|
||
imagePacker.release();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将图片(image.PixelMap)转成jpg、png、webp等
|
||
* 保存图片到指定路径(用户自己选择)
|
||
* @param pixelMap
|
||
* @param format 支持: 'image/jpeg', 'image/png', 'image/webp'
|
||
*/
|
||
static async saveFormatToGallery(pixelMap: image.PixelMap, format: string) {
|
||
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||
const packOpts: image.PackingOption = { format: format, quality: 100 };
|
||
|
||
try {
|
||
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.${format === 'image/jpeg' ? 'jpg' : format.substring(6)}`]);
|
||
|
||
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||
|
||
if (!targetUri) {
|
||
console.warn('用户取消保存或路径获取失败');
|
||
return;
|
||
}
|
||
|
||
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||
fs.writeSync(file.fd, buffer);
|
||
fs.closeSync(file.fd);
|
||
|
||
|
||
ToastUtils.normalToast({ message: '图片已保存' });
|
||
} catch (err) {
|
||
ToastUtils.normalToast({ message: '保存失败' });
|
||
} finally {
|
||
imagePacker.release();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将图片(image.PixelMap)转成SVG
|
||
* @param pixelMap
|
||
*/
|
||
static async saveSvgToGallery(pixelMap: image.PixelMap) {
|
||
let imagePackerApi = image.createImagePacker();
|
||
const info = await pixelMap.getImageInfo();
|
||
const width = info.size.width;
|
||
const height = info.size.height;
|
||
|
||
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.svg`]);
|
||
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||
if (!targetUri) {
|
||
console.warn('用户取消保存或路径获取失败');
|
||
return;
|
||
}
|
||
|
||
const packOptions: image.PackingOption = {
|
||
format: 'image/png',
|
||
quality: 100
|
||
};
|
||
const arrayBuffer = await imagePackerApi.packing(pixelMap, packOptions);
|
||
|
||
// 将二进制数据转为 Base64 字符串,buffer.from().toString('base64') 默认不换行的 (等同于 NO_WRAP)
|
||
const base64Data = buffer.from(arrayBuffer).toString('base64');
|
||
|
||
// 构建 SVG 内容
|
||
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||
<image width="${width}" height="${height}"
|
||
xlink:href="data:image/png;base64,${base64Data}"
|
||
href="data:image/png;base64,${base64Data}" />
|
||
</svg>`;
|
||
|
||
|
||
try {
|
||
let file = fileIo.openSync(targetUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
|
||
fileIo.writeSync(file.fd, svgContent);
|
||
fileIo.closeSync(file);
|
||
ToastUtils.normalToast({ message: '图片已保存' });
|
||
} catch (err) {
|
||
ToastUtils.normalToast({ message: '保存失败' });
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 保存图片(通过布局ID保存图片)
|
||
* 注意:id为控件的ID
|
||
*/
|
||
static async saveImageForID(id: string): Promise<boolean> {
|
||
try {
|
||
let pixelMap = await componentSnapshot.get(id);
|
||
|
||
if (pixelMap) {
|
||
await ImageUtils.savePixelMapToGallery(pixelMap);
|
||
ToastUtils.normalToast({ message: '图片已保存' });
|
||
|
||
return true;
|
||
}else{
|
||
return false;
|
||
}
|
||
} catch (err) {
|
||
console.error(`快照捕获失败: ${JSON.stringify(err)}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打开图库(仅可选单张图片)
|
||
*/
|
||
static async openGallery(): Promise<StickerItem|null> {
|
||
let file: fs.File | null = null;
|
||
try {
|
||
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||
const options = new photoAccessHelper.PhotoSelectOptions();
|
||
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||
options.maxSelectNumber = 1;
|
||
|
||
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||
if (result.photoUris.length === 0) return null;
|
||
|
||
const uri: string = result.photoUris[0];
|
||
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||
let newItem = new StickerItem(pixelMap,0);
|
||
|
||
return newItem;
|
||
} catch (err) {
|
||
ToastUtils.normalToast({ message: '获取图片失败!' });
|
||
return null;
|
||
} finally {
|
||
if (file) fs.closeSync(file);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打开图库(仅可选单张图片-抠图)
|
||
*/
|
||
static async openCutoutGallery(): Promise<StickerItem|null> {
|
||
let file: fs.File | null = null;
|
||
try {
|
||
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||
const options = new photoAccessHelper.PhotoSelectOptions();
|
||
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||
options.maxSelectNumber = 1;
|
||
|
||
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||
if (result.photoUris.length === 0) return null;
|
||
|
||
const uri: string = result.photoUris[0];
|
||
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||
|
||
ToastUtils.normalToast({ message: '抠图中...' });
|
||
|
||
await subjectSegmentation.init();
|
||
const visionInfo: subjectSegmentation.VisionInfo = { pixelMap: pixelMap };
|
||
const config: subjectSegmentation.SegmentationConfig = {
|
||
enableSubjectForegroundImage: true
|
||
};
|
||
|
||
const segResult: subjectSegmentation.SegmentationResult =
|
||
await subjectSegmentation.doSegmentation(visionInfo, config);
|
||
if (segResult && segResult.fullSubject && segResult.fullSubject.foregroundImage) {
|
||
const pixelMap = segResult.fullSubject.foregroundImage;
|
||
let newItem = new StickerItem(pixelMap,0);
|
||
|
||
await subjectSegmentation.release();
|
||
return newItem;
|
||
}else{
|
||
await subjectSegmentation.release();
|
||
return null;
|
||
}
|
||
} catch (err) {
|
||
ToastUtils.normalToast({ message: '抠图失败,请使用规范图片' });
|
||
return null;
|
||
} finally {
|
||
if (file) fs.closeSync(file);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打开图库(可选择多张图片)
|
||
*/
|
||
static async openMultipleGallery(): Promise<image.PixelMap[]> {
|
||
const pixelMaps: image.PixelMap[] = [];
|
||
try {
|
||
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
|
||
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||
photoSelectOptions.maxSelectNumber = 10;
|
||
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||
const photoSelectResult = await photoPicker.select(photoSelectOptions);
|
||
|
||
if (!photoSelectResult || photoSelectResult.photoUris.length === 0) {
|
||
return [];
|
||
}
|
||
for (const uri of photoSelectResult.photoUris) {
|
||
let file: fs.File | null = null;
|
||
try {
|
||
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||
const decodingOptions: image.DecodingOptions = {
|
||
editable: true,
|
||
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
|
||
};
|
||
const pm = await imageSource.createPixelMap(decodingOptions);
|
||
if (pm) {
|
||
pixelMaps.push(pm);
|
||
}
|
||
imageSource.release();
|
||
} catch (e) {
|
||
console.error(`解析图片失败: ${uri}, 错误: ${JSON.stringify(e)}`);
|
||
} finally {
|
||
if (file) {
|
||
fs.closeSync(file.fd);
|
||
}
|
||
}
|
||
}
|
||
return pixelMaps;
|
||
} catch (err) {
|
||
console.error('选择图片过程发生错误: ' + JSON.stringify(err));
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 Resource 对应的 PixelMap
|
||
* @param res Resource 对象
|
||
* @returns PixelMap 对象
|
||
*/
|
||
static async getPixelMapFromResource(res: Resource, context: common.UIAbilityContext): Promise<image.PixelMap> {
|
||
const resourceMgr = context.resourceManager;
|
||
const fileData = await resourceMgr.getMediaContent(res);
|
||
const imageSource = image.createImageSource(fileData.buffer as ArrayBuffer);
|
||
return await imageSource.createPixelMap({
|
||
editable: true,
|
||
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 将 PixelMap 转换为 ImageSource
|
||
* @param pixelMap 输入的位图对象
|
||
* @returns 转换后的 ImageSource 对象
|
||
*/
|
||
static async pixelMapToImageSource(pixelMap: image.PixelMap): Promise<image.ImageSource|undefined> {
|
||
const imagePacker = image.createImagePacker();
|
||
|
||
const packingOptions: image.PackingOption = {
|
||
format: "image/jpeg",
|
||
quality: 100
|
||
};
|
||
|
||
try {
|
||
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packingOptions);
|
||
const imageSource: image.ImageSource = image.createImageSource(buffer);
|
||
|
||
return imageSource;
|
||
} catch (err) {
|
||
console.error('转换失败: ' + JSON.stringify(err));
|
||
return undefined;
|
||
} finally {
|
||
imagePacker.release();
|
||
}
|
||
}
|
||
|
||
}
|