熊熊出没 发表于 2024-7-26 13:20:03

使用HarmonyOS实现图片编辑,裁剪、旋转、亮度、透明度

先容

本篇Codelab是基于ArkTS的声明式开发范式的样例,主要先容了图片编辑实现过程。样例主要包含以下功能:

[*]图片的解码。
[*]使用PixelMap进行图片编辑,如裁剪、旋转、亮度、透明度、饱和度等。
[*]图片的编码。
https://img-blog.csdnimg.cn/direct/0ceab5c737264fad809660e8bda26bc0.gif
相干概念



[*]图片解码:读取差别格式的图片文件,无压缩的解码为位图格式。
[*]PixelMap:图片解码后的状态,用于对图片像素进行处理惩罚。
[*]图片编码:图片颠末像素处理惩罚完成之后,须要重新进行编码打包,生成须要的图片格式。
环境搭建

软件要求



[*]DevEco Studio版本:DevEco Studio 3.1 Release。
[*]OpenHarmony SDK版本:API version 9。
硬件要求



[*]开发板范例:润和RK3568开发板。
[*]OpenHarmony系统:3.2 Release。
环境搭建

完本钱篇Codelab我们起首要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步调进行:

[*]获取OpenHarmony系统版本:标准系统办理方案(二进制)。以3.2 Release版本为例:
https://img-blog.csdnimg.cn/direct/5e1508cd230a40bc8076cfb244555139.png
2.搭建烧录环境。

[*]完成DevEco Device Tool的安装
[*]完成RK3568开发板的烧录
3.搭建开发环境。

[*]开始前请参考工具准备,完成DevEco Studio的安装和开发环境设置。
[*]开发环境设置完成后,请参考使用工程领导创建工程(模板选择“Empty Ability”)。
[*]工程创建完成后,选择使用真机进行调测。
代码结构解读

本篇Codelab只对核心代码进行讲解。
├──entry/src/main/ets                            // 代码区
│├──common                        
││└──constant
││   └──CommonConstant.ets                   // 常量类
│├──entryability
││└──EntryAbility.ts                         // 本地启动ability         
│├──pages
││└──HomePage.ets                            // 本地主页面   
│├──utils
││├──AdjustUtil.ets                        // 调节工具类
││├──CropUtil.ets                            // 裁剪工具类
││├──DecodeUtil.ets                        // 解码工具类
││├──DrawingUtils.ets                        // Canvas画图工具类
││├──EncodeUtil.ets                        // 编码工具类
││├──LoggerUtil.ets                        // 日志工具类
││├──MathUtils.ets                           // 坐标转换工具类
││└──OpacityUtil.ets                         // 透明度调节工具类
│├──view
││├──AdjustContentView.ets                   // 色域调整视图   
││└──ImageSelect.ets                         // Canvas选择框实现类   
│├──viewmodel
││├──CropShow.ets                            // 选择框显示控制类
││├──CropType.ets                            // 按比例选取图片
││├──IconListViewModel.ets                   // icon数据
││├──ImageEditCrop.ets                     // 图片编辑操作类
││├──ImageFilterCrop.ets                     // 图片操作收集类
││├──ImageSizeItem.ets                     // 图片尺寸
││├──Line.ets                              // 线封装类
││├──MessageItem.ets                         // 多线程封装消息
││├──OptionViewModel.ets                     // 图片处理封装类
││├──PixelMapWrapper.ets                     // PixelMap封装类
││├──Point.ets                               // 点封装类
││├──Ratio.ets                               // 比例封装类
││├──Rect.ets                              // 矩形封装类
││├──RegionItem.ets                        // 区域封装类
││└──ScreenManager.ts                        // 屏幕尺寸计算工具类
│└──workers
│   ├──AdjustBrightnessWork.ts               // 亮度异步调节
│   └──AdjustSaturationWork.ts               // 饱和度异步调节
└──entry/src/main/resources                      // 资源文件目录 图片解码

在这个章节中,须要完成图片解码的利用,并将解码后的图片展示。效果如图所示:
https://img-blog.csdnimg.cn/direct/603b601be9114b4e9d6bad8dd70d4a5c.png
在进行图片编辑前须要先加载图片,当前文档是在生命周期aboutToAppear开始加载。具体实现步调。

[*]读取资源文件。
[*]将获取的fd创建成图片实例,通过实例获取其pixelMap。
[*]将解析好的pixelMap通过Image组件加载表现。
// HomePage.ets
aboutToAppear() {
this.pixelInit();
...
}

build() {
Column() {
    ...
    Column() {
      if (this.isCrop && this.showCanvas && this.statusBar > 0) {
      if (this.isSaveFresh) {
          ImageSelect({
            statusBar: this.statusBar
          })
      }
      ...
      } else {
      if (this.isPixelMapChange) {
          Image(this.pixelMap)
            .scale({ x: this.imageScale, y: this.imageScale, z: 1 })
            .objectFit(ImageFit.None)
      }
      ...
      }
    }
    ...
}
...
}

async getResourceFd(filename: string) {
const resourceMgr = getContext(this).resourceManager;
const context = getContext(this);
if (filename === CommonConstants.RAW_FILE_NAME) {
    let imageBuffer = await resourceMgr.getMediaContent($r("app.media.ic_low"))
    let filePath = context.cacheDir + '/' + filename;
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    let writeLen = fs.writeSync(file.fd, imageBuffer.buffer);
    fs.copyFileSync(filePath, context.cacheDir + '/' + CommonConstants.RAW_FILE_NAME_TEST);
    return file.fd;
} else {
    let filePath = context.cacheDir + '/' + filename;
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    return file.fd;
}
}

async getPixelMap(fileName: string) {
const fd = await this.getResourceFd(fileName);
const imageSourceApi = image.createImageSource(fd);
if (!imageSourceApi) {
    Logger.error(TAG, 'imageSourceAPI created failed!');
    return;
}
const pixelMap = await imageSourceApi.createPixelMap({
    editable: true
});
return pixelMap;
} 图片处理惩罚

当前章节须要完成图片的裁剪、旋转、色域调节(本章只先容亮度、透明度、饱和度)等功能。


[*]裁剪:选取图片中的部分进行裁剪生成新的图片。
[*]旋转:将图片按照差别的角度进行旋转,生成新的图片。
[*]色域调节:当前Codelab色域调节的亮度、透明度和饱和度,使用色域模型RGB-HSV来实现的。RGB:是我们接触最多的颜色空间,分别为红色(R),绿色(G)和蓝色(B)。HSV:是用色相H,饱和度S,明亮度V来形貌颜色的变化。H:色相H取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。S:饱和度S越高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。V:明度V表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。
// AdjustUtil.ets
// rgb转hsv
function rgb2hsv(red: number, green: number, blue: number) {
let hsvH: number = 0, hsvS: number = 0, hsvV: number = 0;
const rgbR: number = colorTransform(red);
const rgbG: number = colorTransform(green);
const rgbB: number = colorTransform(blue);
const maxValue = Math.max(rgbR, Math.max(rgbG, rgbB));
const minValue = Math.min(rgbR, Math.min(rgbG, rgbB));
hsvV = maxValue * CommonConstants.CONVERT_INT;
if (maxValue === 0) {
    hsvS = 0;
} else {
    hsvS = Number((1 - minValue / maxValue).toFixed(CommonConstants.DECIMAL_TWO)) * CommonConstants.CONVERT_INT;
}
if (maxValue === minValue) {
    hsvH = 0;
}
if (maxValue === rgbR && rgbG >= rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)));
}
if (maxValue === rgbR && rgbG < rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)) + CommonConstants.ANGLE_360);
}
if (maxValue === rgbG) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbB - rgbR) / (maxValue - minValue)) + CommonConstants.ANGLE_120);
}
if (maxValue === rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbR - rgbG) / (maxValue - minValue)) + CommonConstants.ANGLE_240);
}
return ;
}
// hsv转rgb
function hsv2rgb(hue: number, saturation: number, value: number) {
let rgbR: number = 0, rgbG: number = 0, rgbB: number = 0;
if (saturation === 0) {
    rgbR = rgbG = rgbB = Math.round((value * CommonConstants.COLOR_LEVEL_MAX) / CommonConstants.CONVERT_INT);
    return { rgbR, rgbG, rgbB };
}
const cxmC = (value * saturation) / (CommonConstants.CONVERT_INT * CommonConstants.CONVERT_INT);
const cxmX = cxmC * (1 - Math.abs((hue / CommonConstants.ANGLE_60) % CommonConstants.MOD_2 - 1));
const cxmM = (value - cxmC * CommonConstants.CONVERT_INT) / CommonConstants.CONVERT_INT;
const hsvHRange = Math.floor(hue / CommonConstants.ANGLE_60);
switch (hsvHRange) {
    case AngelRange.ANGEL_0_60:
      rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_60_120:
      rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_120_180:
      rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_180_240:
      rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_240_300:
      rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_300_360:
      rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    default:
      break;
}
return [
    Math.round(rgbR),
    Math.round(rgbG),
    Math.round(rgbB)
];
} 图片裁剪


[*]通过pixelMap获取图片尺寸,为后续裁剪做准备。
[*]确定裁剪的方式,当前裁剪默认有自由选取、1:1选取、4:3选取、16:9选取。
[*]通过pixelMap调用接口crop()进行裁剪利用。
说明: 当前裁剪功能采用pixelMap裁剪能力直接做切割,会有叠加效果,后续会通过增加选取框对当前功能进行优化。
https://img-blog.csdnimg.cn/direct/1f35b351549c4294bd7ee78ca234095d.png
// HomePage.ets
cropImage(index: CropType) {
this.currentCropIndex = index;
switch (this.currentCropIndex) {
    case CropType.ORIGINAL_IMAGE:
      this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
      break;
    case CropType.SQUARE:
      this.cropRatio = CropRatioType.RATIO_TYPE_1_1;
      break;
    case CropType.BANNER:
      this.cropRatio = CropRatioType.RATIO_TYPE_4_3;
      break;
    case CropType.RECTANGLE:
      this.cropRatio = CropRatioType.RATIO_TYPE_16_9;
      break;
    default:
      this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
      break;
}
}

// ImageFilterCrop.ets
cropImage(pixelMap: PixelMapWrapper, realCropRect: RectF, callback: () => void) {
let offWidth = realCropRect.getWidth();
let offHeight = realCropRect.getHeight();
if (pixelMap.pixelMap!== undefined) {
    pixelMap.pixelMap.crop({
      size:{ height: vp2px(offHeight), width: vp2px(offWidth) },
      x: vp2px(realCropRect.left),
      y: vp2px(realCropRect.top)
    }, callback);
}
} 图片旋转


[*]确定旋转方向,当前支持顺时针和逆时针旋转。
[*]通过pixelMap调用接口rotate()进行旋转利用。
https://img-blog.csdnimg.cn/direct/8318ce31ceee46ad937a87dbe54cc2cf.png
// HomePage.ets
rotateImage(rotateType: RotateType) {
if (rotateType === RotateType.CLOCKWISE) {
    try {
      if (this.pixelMap !== undefined) {
      this.pixelMap.rotate(CommonConstants.CLOCK_WISE)
          .then(() => {
            this.flushPixelMapNew();
          })
      }
    } catch (error) {
      Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
    }
}
if (rotateType === RotateType.ANTI_CLOCK) {
    try {
      if (this.pixelMap !== undefined) {
      this.pixelMap.rotate(CommonConstants.ANTI_CLOCK)
          .then(() => {
            this.flushPixelMapNew();
          })
      }
    } catch (error) {
      Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
    }
}
} 亮度调节


[*]将pixelMap转换成ArrayBuffer。
[*]将生成好的ArrayBuffer发送到worker线程。
[*]对每一个像素点的亮度值按倍率计算。
[*]将计算好的ArrayBuffer发送回主线程。
[*]将ArrayBuffer写入pixelMap,刷新UI。
说明: 当前亮度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。
https://img-blog.csdnimg.cn/direct/e497c303b1fa409a9d1a0b423afaee9c.png
// AdjustContentView.ets
// 转化成pixelMap及发送buffer到worker,返回数据刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
try {
    let workerInstance = new worker.ThreadWorker(workerName);
    const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
    this.pixelMap.readPixelsToBuffer(bufferArray).then(() => {
      let message = new MessageItem(bufferArray, sliderValue, value);
      workerInstance.postMessage(message);
      if (this.postState) {
      this.deviceListDialogController.open();
      }
      this.postState = false;
      workerInstance.onmessage = (event: MessageEvents) => {
      this.updatePixelMap(event)
      };
      if (type === AdjustId.BRIGHTNESS) {
      this.brightnessLastSlider = Math.round(value);
      } else {
      this.saturationLastSlider = Math.round(value);
      }
      workerInstance.onexit = () => {
      if (workerInstance !== undefined) {
          workerInstance.terminate();
      }
      }
    });
} catch (error) {
    Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`)
}
}

// AdjustBrightnessWork.ts
// worker线程处理部分
workerPort.onmessage = function(event : MessageEvents) {
let bufferArray = event.data.buf;
let last = event.data.last;
let cur = event.data.cur;
let buffer = adjustImageValue(bufferArray, last, cur);
workerPort.postMessage(buffer);
workerPort.close();
}

// AdjustUtil.ets
// 倍率计算部分
export function adjustImageValue(bufferArray: ArrayBuffer, last: number, cur: number) {
return execColorInfo(bufferArray, last, cur, HSVIndex.VALUE);
} 透明度调节


[*]获取pixelMap。
[*]调用接口opacity()进行透明度调节。
https://img-blog.csdnimg.cn/direct/10ef16fb1669407fade73ff3555f1d7e.png
// OpacityUtil.ets
export async function adjustOpacity(pixelMap: PixelMap, value: number) {
if (!pixelMap) {
    return;
}
const newPixelMap = pixelMap;
await newPixelMap.opacity(value / CommonConstants.SLIDER_MAX);
return newPixelMap;
} 饱和度调节


[*]将pixelMap转换成ArrayBuffer。
[*]将生成好的ArrayBuffer发送到worker线程。
[*]对每一个像素点的饱和度按倍率计算。
[*]将计算好的ArrayBuffer发送回主线程。
[*]将ArrayBuffer写入pixelMap,刷新UI。
说明: 当前饱和度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。
https://img-blog.csdnimg.cn/direct/46d4c572a5e2414385af7ac4a5e4056e.png
// AdjustContentView.ets
// 转化成pixelMap及发送buffer到worker,返回数据刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
try {
    let workerInstance = new worker.ThreadWorker(workerName);
    const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
    this.pixelMap.readPixelsToBuffer(bufferArray).then(() => {
      let message = new MessageItem(bufferArray, sliderValue, value);
      workerInstance.postMessage(message);
      if (this.postState) {
      this.deviceListDialogController.open();
      }
      this.postState = false;
      workerInstance.onmessage = (event: MessageEvents) => {
      this.updatePixelMap(event)
      };
      if (type === AdjustId.BRIGHTNESS) {
      this.brightnessLastSlider = Math.round(value);
      } else {
      this.saturationLastSlider = Math.round(value);
      }
      workerInstance.onexit = () => {
      if (workerInstance !== undefined) {
          workerInstance.terminate();
      }
      }
    });
} catch (error) {
    Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`);
}
}

// AdjustSaturationWork.ts
// worker线程处理部分
workerPort.onmessage = function(event : MessageEvents) {
let bufferArray = event.data.buf;
let last = event.data.last;
let cur = event.data.cur;
let buffer = adjustSaturation(bufferArray, last, cur)
workerPort.postMessage(buffer);
workerPort.close();
}

// AdjustUtil.ets
// 倍率计算部分
export function adjustSaturation(bufferArray: ArrayBuffer, last: number, cur: number) {
return execColorInfo(bufferArray, last, cur, HSVIndex.SATURATION);
} 图片编码

图片位图颠末处理惩罚之后,还是属于解码的状态,还须要进行打包编码成对应的格式,本章讲解编码的具体过程。

[*]通过image组件创建打包工具packer。
[*]使用PackingOption进行打包参数设定,比如格式、压缩质量等。
[*]打包成图片信息数据imageData。
[*]创建媒体库media,获取公共路径。
[*]创建媒体文件asset,获取其fd。
[*]使用fs将打包好的图片数据写入到媒体文件asset中。
// ImageSelect.ets
async encode(pixelMap: PixelMap | undefined) {
if (pixelMap === undefined) {
    return;
}

const newPixelMap = pixelMap;
// 打包图片
const imagePackerApi = image.createImagePacker();
const packOptions: image.PackingOption = {
    format: CommonConstants.ENCODE_FORMAT,
    quality: CommonConstants.ENCODE_QUALITY
}
const imageData = await imagePackerApi.packing(newPixelMap, packOptions);
Logger.info(TAG, `imageData's length is ${imageData.byteLength}`);
// 获取相册路径
const context = getContext(this);
const media = mediaLibrary.getMediaLibrary(context);
const publicPath = await media.getPublicDirectory(mediaLibrary.DirectoryType.DIR_IMAGE);
const currentTime = new Date().getTime();
// 创建图片资源
const imageAssetInfo = await media.createAsset(
    mediaLibrary.MediaType.IMAGE,
    `${CommonConstants.IMAGE_PREFIX}_${currentTime}${CommonConstants.IMAGE_FORMAT}`,
    publicPath
);
const imageFd = await imageAssetInfo.open(CommonConstants.ENCODE_FILE_PERMISSION);
await fs.write(imageFd, imageData);
// 释放资源
await imageAssetInfo.close(imageFd);
imagePackerApi.release();
await media.release();
} 总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

[*]使用image库进行图片的解码。
[*]使用pixelMap进行图片处理惩罚(旋转、裁剪、亮度、透明度、饱和度等)。
[*]使用image库进行图片的编码。
为了帮助大家更深入有效的学习到鸿蒙开发知识点,小编特意给大家准备了一份全套最新版的HarmonyOS NEXT学习资源,获取完备版方式请点击→《HarmonyOS讲授视频》
HarmonyOS讲授视频:语法ArkTS、TypeScript、ArkUI等…视频教程

https://img-blog.csdnimg.cn/direct/a3e524e690cd492dbc767064caa64548.png
https://img-blog.csdnimg.cn/direct/10c83f8a84c048f09117e3221bbb456a.png

鸿蒙生态应用开发白皮书V2.0PDF:

获取完备版白皮书方式请点击→《鸿蒙生态应用开发白皮书V2.0PDF》
https://img-blog.csdnimg.cn/direct/19d94273cdb74293a6adb5f71d87c736.png
鸿蒙 (Harmony OS)开发学习手册

一、入门必看

[*]应用开发导读(ArkTS)
[*].……
https://img-blog.csdnimg.cn/direct/1036407103d14346964c012d053c8005.png

二、HarmonyOS 概念

[*]系统定义
[*]技术架构
[*]技术特性
[*]系统安全
[*]…
https://img-blog.csdnimg.cn/direct/fffdec1ebe3240328e20c525efab4668.png
三、如何快速入门?《鸿蒙底子入门学习指南》

[*]基本概念
[*]构建第一个ArkTS应用
[*].……
https://img-blog.csdnimg.cn/direct/50977da98d3b4242934c89f95d7f1620.png

四、开发底子知识

[*]应用底子知识
[*]设置文件
[*]应用数据管理
[*]应用安全管理
[*]应用隐私保护
[*]三方应用调用管控机制
[*]资源分类与访问
[*]学习ArkTS语言
[*].……
https://img-blog.csdnimg.cn/direct/721912eab7414826adef69b5307174fc.png

五、基于ArkTS 开发

[*]Ability开发
[*]UI开发
[*]公共变乱与通知
[*]窗口管理
[*]媒体
[*]安全
[*]7.网络与链接
[*]电话服务
[*]数据管理
[*]后台使命(Background Task)管理
[*]装备管理
[*]装备使用信息统计
[*]DFX
[*]国际化开发
[*]折叠屏系列
[*].……
https://img-blog.csdnimg.cn/direct/e177ff19962549219f0507c7669f9d91.png

更多了解更多鸿蒙开发的相干知识可以参考:《鸿蒙 (Harmony OS)开发学习手册》

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 使用HarmonyOS实现图片编辑,裁剪、旋转、亮度、透明度