HarmonyOS开发案例:【图片编辑】

news/2024/10/9 3:47:24/

介绍

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

  1. 图片的解码。
  2. 使用PixelMap进行图片编辑,如裁剪、旋转、亮度、透明度、饱和度等。
  3. 图片的编码。

相关概念

  • [图片解码]:读取不同格式的图片文件,无压缩的解码为位图格式。
  • [PixelMap]:图片解码后的状态,用于对图片像素进行处理。
  • [图片编码]:图片经过像素处理完成之后,需要重新进行编码打包,生成需要的图片格式。

环境搭建

软件要求

  • [DevEco Studio]版本:DevEco Studio 3.1 Release。
  • OpenHarmony SDK版本:API version 9。

硬件要求

  • 开发板类型:[润和RK3568开发板]。
  • OpenHarmony系统:3.2 Release。

环境搭建

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

  1. [获取OpenHarmony系统版本]:标准系统解决方案(二进制)。以3.2 Release版本为例:

  2. 搭建烧录环境。

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

    1. 开始前请参考[工具准备],完成DevEco Studio的安装和开发环境配置。
    2. 开发环境配置完成后,请参考[使用工程向导]创建工程(模板选择“Empty Ability”)。
    3. 工程创建完成后,选择使用[真机进行调测]。
    4. HarmonyOS与OpenHarmony鸿蒙文档籽料:mau123789是v直接拿

搜狗高速浏览器截图20240326151450.png

代码结构解读

本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在gitee中提供。

├──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                      // 资源文件目录

鸿蒙开发指导文档:gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。

图片解码

在这个章节中,需要完成图片解码的操作,并将解码后的图片展示。效果如图所示:

在进行图片编辑前需要先加载图片,当前文档是在生命周期aboutToAppear开始加载。具体实现步骤。

  1. 读取资源文件。
  2. 将获取的fd创建成图片实例,通过实例获取其pixelMap。
  3. 将解析好的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:是我们接触最多的颜色空间,分别为红色®,绿色(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 [hsvH, hsvS, hsvV];
}
// 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)];
}

图片裁剪

  1. 通过pixelMap获取图片尺寸,为后续裁剪做准备。
  2. 确定裁剪的方式,当前裁剪默认有自由选取、1:1选取、4:3选取、16:9选取。
  3. 通过pixelMap调用接口crop()进行裁剪操作。

说明: 当前裁剪功能采用pixelMap裁剪能力直接做切割,会有叠加效果,后续会通过增加选取框对当前功能进行优化。

// 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);}
}

图片旋转

  1. 确定旋转方向,当前支持顺时针和逆时针旋转。
  2. 通过pixelMap调用接口rotate()进行旋转操作。

// 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}`);}}
}

亮度调节

  1. 将pixelMap转换成ArrayBuffer。
  2. 将生成好的ArrayBuffer发送到worker线程。
  3. 对每一个像素点的亮度值按倍率计算。
  4. 将计算好的ArrayBuffer发送回主线程。
  5. 将ArrayBuffer写入pixelMap,刷新UI。

说明: 当前亮度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。

// 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);
}

透明度调节

  1. 获取pixelMap。
  2. 调用接口opacity()进行透明度调节。

// 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;
}

饱和度调节

  1. 将pixelMap转换成ArrayBuffer。
  2. 将生成好的ArrayBuffer发送到worker线程。
  3. 对每一个像素点的饱和度按倍率计算。
  4. 将计算好的ArrayBuffer发送回主线程。
  5. 将ArrayBuffer写入pixelMap,刷新UI。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 说明: 当前饱和度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。

// 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);
}

图片编码

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

  1. 通过image组件创建打包工具packer。
  2. 使用PackingOption进行打包参数设定,比如格式、压缩质量等。
  3. 打包成图片信息数据imageData。
  4. 创建媒体库media,获取公共路径。
  5. 创建媒体文件asset,获取其fd。
  6. 使用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();
}

http://www.ppmy.cn/news/1436308.html

相关文章

C++ 之 string类 详细讲解

喜欢的人有点难追怎么办 那就直接拉黑 七个女生在一起是七仙女&#xff0c;那七个男生在一起是什么&#xff1f; 葫芦七兄弟 目录 一、为什么要学习string类 二、标准库中的string类 1.string类 2.string类的常用接口说明 2.1 string类对象的常见构造 2.2 string类对…

【数据结构】队列的使用方法

队列&#xff08;Queue&#xff09;是另一种基本的线性数据结构&#xff0c;它允许在一端进行插入操作&#xff0c;而在另一端进行删除操作。队列的特点是先进先出&#xff08;First In First Out, FIFO&#xff09;&#xff0c;即最先进入队列的元素最先被取出。 队列可以用数…

使用Shell终端访问Linux

一、实验目的 1、熟悉Linux文件系统访问命令&#xff1b; 2、熟悉常用 Linux Shell的命令&#xff1b; 3、熟悉在Linux文件系统中vi编辑器的使用&#xff1b; 4、进一步熟悉虚拟机网络连接模式与参数配置&#xff01; 二、实验内容 1、使用root帐号登陆到Linux的X-windows…

MySQL的事务相关的语句的使用

MySQL的事务相关的语句的使用 事务是数据库管理系统执行过程中的一个程序单位&#xff0c;由一个或多个数据库操作组成。MySQL作为一款流行的关系型数据库管理系统&#xff0c;支持事务处理&#xff0c;允许用户定义一系列的操作&#xff0c;这些操作要么完全执行&#xff0c;…

LabVIEW学习记录2 - MySQL数据库连接与操作

LabVIEW学习记录2 - MySQL数据库连接与操作 一、前期准备1.1 windows下安装MySQL的ODBC驱动 二、LabVIEW创建MySQL 的UDL文件三、LabVIEW使用UDL文件进行MySQL数据库操作3.1 建立与数据库的连接&#xff1a;DB Tools Open Connection.vi3.2 断开与数据库的连接&#xff1a;DB T…

Ubuntu下,Notepad++的安装、汉化与卸载

Notepad的作者有自己的问题&#xff0c;但必须承认的是软件本身质量还是不错的&#xff0c;有朋友感到介意&#xff0c;可以了解另一款软件&#xff1a;Notepad--&#xff0c;目前也在逐步优化中 1.安装 在终端中输入指令 sudo snap install notepad-plus-plus 等待安装即可…

深耕“星光电务”党建品牌 引领保障企业高质量发展

在日前闭幕的2024年首届全国企业党务工作者论坛中&#xff0c;中铁十一局集团电务工程有限公司提交的论文《深耕“星光电务”党建品牌 引领保障企业高质量发展》荣获优秀论文奖。该论文由陈柯、刘敏之、徐干、姜亦珂联合撰写&#xff0c;展示了他们在党建工作中的创新实践与显著…

Linux——web基础实验

实验前的安装 [rootwebserver ~]# yum -y install httpd [rootwebserver ~]# systemctl enable --now httpd Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service. [rootwebserver ~]# echo test for apach…