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

介绍

本篇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://xiahunao.cn/news/2981706.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

数学建模完整版

模型与适用题型 微分方程传染病预测模型 神经网络 层次分析法 粒子群算法 matlab 优劣解距离法

11 JavaScript学习:事件

Html事件 HTML 中有很多事件可以用来与用户交互&#xff0c;以下是一些常见的 HTML 事件及其详细解释和举例&#xff1a; click 事件&#xff1a;当用户点击元素时触发。 <button onclick"myFunction()">点击我</button>dblclick 事件&#xff1a;当用…

各平台奇怪问题备忘录

微信小程序 小程序报错Page 页面路径 has not been register yet 描述&#xff1a;uniapp做微信小程序开发时&#xff0c;新增某页面后&#xff0c;小程序跳转该页面报错Page 页面路径 has not been register yet 已知&#xff1a;page.json已添加该页面&#xff0c;小程序a…

游戏陪玩系统app

游戏陪玩系统APP为用户提供了一个便捷的平台&#xff0c;让他们能够轻松找到合适的陪玩者&#xff0c;一同享受游戏的乐趣。以下是对您提到的功能的详细解释&#xff1a; 游戏约玩&#xff1a; 在陪玩APP上&#xff0c;用户可以浏览陪玩者的信息&#xff0c;包括他们的游戏技能…

DNS 解析过程

一张图即可说明&#xff0c;如果能看到上图就不用往下看了。下面是上图的文字版解析 a. 用户输入 URL: 当你在浏览器中点击一个链接或输入一个网址时&#xff0c;比如点击一个图片链接 http://www.example.com/image.jpg&#xff0c;这个过程开始于你的设备尝试理解这个地址所…

Excel 冻结前几行

Excel中有冻结首航和冻结首列的选项&#xff0c;但是如果想冻结前几行该怎么操作&#xff1f; 冻结首行或冻结首列 视图 -> 冻结窗格 -> 冻结首行或冻结首列 冻结前几行或前几列 视图 -> 冻结窗格 -> 冻结拆分窗格 具体冻结几行和几列取决于当前选中的单元格。…

使用甘特图来做时间管理

在这个追求效率的时代,掌握高超的时间管理技能几乎等同于掌控了成功。事实上,时间就是金钱,更是稀缺资源。那么,如何高效地规划和利用时间呢?甘特图应该是您的必备武器之一。 甘特图(Gantt chart)名字虽然有些陌生,但它的使用范围确实广泛。无论是全职妈妈安排家务,还是上市公…

数据库轻松切换:解读Spring中的AbstractRoutingDataSource

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 数据库轻松切换&#xff1a;解读Spring中的AbstractRoutingDataSource 前言AbstractRoutingDataSource介绍作用和优势&#xff1a;作用&#xff1a;优势&#xff1a; 使用 AbstractRoutingDataSource …

flutter release 报错 Error: SocketException: Failed host lookup:

flutter 的 debug 模式没有任何问题 &#xff0c;打了release 包后一直报下面的错&#xff0c;查了一下是 因为没有网络权限 Error: SocketException: Failed host lookup: yomi-test-aws-sg.yomigame.games (OS Error: No address associated with hostname, errno 7) 按照下…

【Vue】常见的七大属性(描述+案例)

一、前言 最近&#xff0c;因为项目需要自己就去学习了一下Vue的相关知识&#xff0c;自己花了几天&#xff0c;结合官方文档和相应的视频学习了一下Vue,了解了Vue大概的一些属性&#xff0c;方法&#xff0c;特点等。接下来博主会将自己学习的相关内容通过博客的形式进行记录…

python实现钉钉通讯录导出Excel表

Python工具开源专栏 Py0004 python实现钉钉通讯录导出Excel表 Python工具开源专栏前言目录结构部分演示完整代码已在GitHub上开源 前言 需求来源于公司&#xff0c;需要将钉钉通讯录以Excel表的形式导出到本地&#xff0c;方便定期备份。导出的Excel需要处理钉钉用户兼任多部门…

W801学习笔记十一:掌机进阶V3版本之硬件改造

经由前面的笔记&#xff0c;我们打造出了一款游戏掌机。 W801学习笔记十&#xff1a;HLK-W801制作学习机/NES游戏机(总结) 然而&#xff0c;考虑到后续的游戏开发&#xff0c;总是忧心容量不足。故而&#xff0c;在正式展开软件开发工作以前&#xff0c;最终进行一下升级改造…

Ai终端程序推荐waveterm

有没有人根据Ai的风口&#xff0c;做智能终端呢&#xff1f;答案是真有&#xff0c;真有专为无缝工作流程而构建的开源 AI 原生终端。它是去年推出的一个产品&#xff0c;专为程序员和开发者设计&#xff0c;比较创新的一点是所有的工作可以在终端完成&#xff0c;开源和可扩展…

Python静态资源库之webassets使用详解

概要 Python webassets库是一个用于管理静态资源的工具,如CSS、JavaScript等,帮助开发者更有效地管理和优化网站的静态文件。本文将介绍如何安装和使用Python webassets库,以及它的特性、基本功能、高级功能、实际应用场景和总结部分。 安装 首先,需要安装Python webasse…

<前端>Electron-builder为公证后的app打更新信息latest.yml

MacOS下&#xff0c;Electron-builder可以很方便的为测试包app打更新信息&#xff08;latest-mac.yml&#xff09;。 但是&#xff0c;正式发布的时候&#xff0c;不可能用测试包app&#xff0c;因为还没有进行公证。如何为公证的app打latest-mac.yml呢。 其实观察latest-mac.y…

StartAI智能绘图软件出现“缺少Python运行库”怎么办?

StartAI做为一款国产AI界的新秀&#xff0c;是一款贴合AIGC新手的智能绘图软件。新手安装遇见“缺少Python运行库”怎么办”&#xff1f;小编一招搞定~ 解决方法&#xff1a;手动下载【resource文件】&#xff0c;将文件添加到安装目录下。 点击链接进行手动下载噢~ 确保 Star…

Pytest精通指南(28)钩子函数-测试报告(pytest-html)

文章目录 前言应用场景插件安装参数分析使用方法拓展-定制化报告 前言 在软件开发过程中&#xff0c;测试是确保代码质量的关键环节。 而测试报告则是测试过程中不可或缺的输出物&#xff0c;它为我们提供了关于测试用例执行情况的详细信息&#xff0c;帮助我们快速定位和解决问…

AGON爱攻×保时捷设计第三代OLED超宽屏PD49震撼上市!

一次科技与艺术的审美共振&#xff0c;开启多维度、沉浸式感官之旅&#xff01; 科技与艺术的融合&#xff0c;引领着美学新时代。4月22日&#xff0c;全球电竞显示器销量冠军品牌AGON爱攻与欧洲最具创造力的设计工作室保时捷设计再度携手&#xff0c;重磅推出AGON爱攻保时捷设…

比亚迪唐EV和唐DM-p荣耀版上市,成为新能源汽车市场中的佼佼者!

随着环保理念的深入人心&#xff0c;新能源汽车市场正迎来前所未有的发展机遇。在这个变革的浪潮中&#xff0c;唐EV和唐DM-p荣耀版的上市无疑为市场注入了新的活力。它们凭借先进的技术、卓越的性能以及豪华配置&#xff0c;成为了新能源汽车市场中的佼佼者。然而&#xff0c;…

数据结构(七)---树

目录 一.树的基本概念 二.树的性质 三.二叉树 1.二叉树的基本概念 2.特殊的二叉树 &#xff08;1&#xff09;满二叉树 &#xff08;2&#xff09;完全二叉树 &#xff08;3&#xff09;二叉排序树 &#xff08;4&#xff09;平衡二叉树 3.二叉树的性质 4.完全二叉树…