# Image **Repository Path**: niumenglin/image ## Basic Information - **Project Name**: Image - **Description**: 【鸿蒙 Harmony Next 示例 代码】本实例主要展示了图片应用场景相关demo。主要包括了图片预览、图片编辑美化、场景变化前后对比、图片切割九宫格、两张图片拼接、AI抠图、图片加水印等场景示例。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-10 - **Last Updated**: 2025-03-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 图片场景化demo ### 介绍 本实例主要展示了图片应用场景相关demo。主要包括了图片预览、图片编辑美化、场景变化前后对比、图片切割九宫格、两张图片拼接、AI抠图、图片加水印等场景示例。 ### 相关权限 不涉及 ## 图片预览 ### 使用说明 点击图片,进入图片预览界面。可对图片进行缩放、拖拽等操作。 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E5%9B%BE%E7%89%87%E9%A2%84%E8%A7%88.gif) ### 核心代码文件路径 image_operation/src/main/ets/components/ImageViewerComponent.ets ### 实现思路 1、给图片组件的scale、width、height、offset等属性绑定相关响应式变量 ``` Image(item) .width(`calc(100% * ${this.activeImage.scale})`) .height(`calc(100% * ${this.activeImage.scale})`) .objectFit(ImageFit.Contain) .draggable(false) .scale(this.active === i ? { x: this.activeImage.scale, y: this.activeImage.scale } : null) .offset(this.active === i ? { x: this.activeImage.offsetX, y: this.activeImage.offsetY } : null) .onComplete(e => { if (e?.loadingStatus) { // 记录每张图片的大小 this.imageListSize[i] = { width: px2vp(Number(e.contentWidth)), height: px2vp(Number(e.contentHeight)) } // 记录当前选中图片的大小,方便后面手势操作计算 if (this.active === i) { this.activeImage.width = this.imageListSize[i].width this.activeImage.height = this.imageListSize[i].height } } }) ``` 2、监听相关手势操作并执行相关操作逻辑,如:二指缩放、单指滑动、双击等 ``` // 双指操作 PinchGesture({ fingers: 2 }) .onActionStart((e) => { this.defaultScale = this.activeImage.scale }) .onActionUpdate((e) => { let scale = e.scale * this.defaultScale // 计算缩放比例及相应偏移距离 if (scale <= 4 && scale >= 1) { this.activeImage.offsetX = this.activeImage.offsetX / (this.activeImage.scale - 1) * (scale - 1) || 0 this.activeImage.offsetY = this.activeImage.offsetY / (this.activeImage.scale - 1) * (scale - 1) || 0 this.activeImage.offsetStartX = this.activeImage.offsetX this.activeImage.offsetStartY = this.activeImage.offsetY this.activeImage.scale = scale } }) // 单指滑动 PanGesture() .onActionStart(e => { // 记录起始位置 this.activeImage.dragOffsetX = e.fingerList[0].globalX this.activeImage.dragOffsetY = e.fingerList[0].globalY }) .onActionUpdate((e) => { if (this.activeImage.scale === 1) { return } if(!e.fingerList[0]){ return } // 计算移动距离 let offsetX = e.fingerList[0].globalX - this.activeImage.dragOffsetX + this.activeImage.offsetStartX let offsetY = e.fingerList[0].globalY - this.activeImage.dragOffsetY + this.activeImage.offsetStartY if (this.activeImage.width * this.activeImage.scale > this.containerWidth && (this.activeImage.width * this.activeImage.scale - this.containerWidth) / 2 >= Math.abs(offsetX)) { this.activeImage.offsetX = offsetX } if (this.activeImage.height * this.activeImage.scale > this.containerHeight && (this.activeImage.height * this.activeImage.scale - this.containerHeight) / 2 >= Math.abs(offsetY)) { this.activeImage.offsetY = offsetY } if ((this.activeImage.width * this.activeImage.scale - this.containerWidth) / 2 < Math.abs(offsetX)) { this.disabledSwipe = false } }) .onActionEnd((e) => { // 记录当前偏移位置,作为下次操作的其实位置 this.activeImage.offsetStartX = this.activeImage.offsetX this.activeImage.offsetStartY = this.activeImage.offsetY }) .onActionCancel(() => { // 记录当前偏移位置,作为下次操作的其实位置 this.activeImage.offsetStartX = this.activeImage.offsetX this.activeImage.offsetStartY = this.activeImage.offsetY }), //双击手势 TapGesture({ count: 2 }) .onAction(() => { // 缩小 if (this.activeImage.scale > 1) { this.activeImage.scale = 1 this.activeImage.offsetX = 0 this.activeImage.offsetY = 0 this.activeImage.offsetStartX = 0 this.activeImage.offsetStartY = 0 this.disabledSwipe = false } else { // 放大 this.activeImage.scale = 2 this.disabledSwipe = true } }), //单击手势 TapGesture({ count: 1 }) .onAction(() => { this.closePreviewFn() }), ) ) ``` ## 图片编辑美化 ### 使用说明 可对图片编辑,包含裁剪、旋转、色域调节(本章只介绍亮度、透明度、饱和度)、滤镜等功能。 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E5%9B%BE%E7%89%87%E7%BC%96%E8%BE%91%E7%BE%8E%E5%8C%96.gif) ### 核心代码文件路径 image_operation/src/main/ets/edit/Index.ets entry/src/main/ets/utils/AdjustUtil.ets entry/src/main/ets/utils/FilterUtil.ets ### 实现思路 1、通过pixelMap的crop方法来裁剪图片 ``` export async function cropImage(pixelMap: image.PixelMap, x = 0, y = 0, width = 300, height = 300) { // x:裁剪起始点横坐标 // y:裁剪起始点纵坐标 // height:裁剪高度,方向为从上往下 // width:裁剪宽度,方向为从左到右 await pixelMap.crop({ x, y, size: { height, width } }) } ``` 2、通过pixelMap的rotate方法来旋转图片 ``` export async function rotateImage(pixelMap: PixelMap, rotateAngle = 90) { await pixelMap.rotate(rotateAngle); } ``` 3、通过对每个像素点的rgb值转换来改变亮度、透明度 ```c export function execColorInfo(bufferArray: ArrayBuffer, last: number, cur: number, hsvIndex: number) { if (!bufferArray) { return; } const newBufferArr = bufferArray; let colorInfo = new Uint8Array(newBufferArr); for (let i = 0; i < colorInfo?.length; i += CommonConstants.PIXEL_STEP) { // rgb转换成hsv const hsv = rgb2hsv(colorInfo[i + RGBIndex.RED], colorInfo[i + RGBIndex.GREEN], colorInfo[i + RGBIndex.BLUE]); let rate = cur / last; hsv[hsvIndex] *= rate; // hsv转换成rgb const rgb = hsv2rgb(hsv[HSVIndex.HUE], hsv[HSVIndex.SATURATION], hsv[HSVIndex.VALUE]); colorInfo[i + RGBIndex.RED] = rgb[RGBIndex.RED]; colorInfo[i + RGBIndex.GREEN] = rgb[RGBIndex.GREEN]; colorInfo[i + RGBIndex.BLUE] = rgb[RGBIndex.BLUE]; } return newBufferArr; } ``` 4、通过effectKit来添加滤镜 ``` export async function pinkColorFilter(pixelMap: PixelMap) { const pinkColorMatrix: Array = [ 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0 ] const pixelMapFiltered = await effectKit.createEffect(pixelMap).setColorMatrix(pinkColorMatrix).getEffectPixelMap(); return pixelMapFiltered; } ``` ## 场景变化前后对比 ### 使用说明 通过拖拽移动按钮,来展示两张图片的不同 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E5%9C%BA%E6%99%AF%E5%8F%98%E5%8C%96%E5%89%8D%E5%90%8E%E5%AF%B9%E6%AF%94.gif) ### 核心代码文件路径 entry/src/main/ets/views/DragToSwitchPicturesView.ets ### 实现思路 1.创建三个Stack组件,用来展示装修前后对比图,第一个和第三个Stack分别存放装修前的图片和装修后的图片,zIndex设置为1。第二个Stack存放按钮的图片,zIndex设置为2,这样按钮的图片就会覆盖在两张装修图片之上。 ```c Row() { /** * 创建两个Stack组件,用来展示装修前后对比图,分别存放装修前的图片和装修后的图片,zIndex设置为1。 * 中间Column存放按钮的图片,zIndex设置为2,这样按钮的图片就会覆盖在两张装修图片之上。 */ Stack() { ... } .zIndex(CONFIGURATION.Z_INDEX1) .width(this.leftImageWidth) .clip(true) .alignContent(Alignment.TopStart) Column() { Image($r("app.media.drag_to_switch_pictures_drag_button")) .width(30) .height(160) .draggable(false) ... } .width(2) .zIndex(CONFIGURATION.Z_INDEX2) Stack() { ... } .zIndex(CONFIGURATION.Z_INDEX1) .clip(true) .width(this.rightImageWidth) .alignContent(Alignment.TopEnd) } ``` 2.将Image组件放在Row容器里,将Row容器的宽度设置为状态变量,再利用clip属性对于Row容器进行裁剪。 ``` Row() { Image($r("app.media.drag_to_switch_pictures_before_decoration")) .width(320) .height(160) .draggable(false) } .width(this.leftImageWidth) .zIndex(CONFIGURATION.Z_INDEX1) .clip(true) .borderRadius({ topLeft: 10, bottomLeft: 10 }) ``` 3.右边的Image组件与左边同样的操作,但是新增了一个direction属性,使元素从右至左进行布局,为的是让Row从左侧开始裁剪。 ```c Row() { Image($r("app.media.drag_to_switch_pictures_after_decoration")) .width(320) .height(160) .draggable(false) } .width(this.rightImageWidth) .clip(true) .zIndex(CONFIGURATION.Z_INDEX1) .direction(Direction.Rtl) .borderRadius({ topRight: 10, bottomRight: 10 }) ``` 4.中间的Image组件通过手势事件中的滑动手势对Image组件滑动进行监听,对左右Image组件的宽度进行计算从而重新布局渲染。 ```c Image($r("app.media.drag_to_switch_pictures_drag_button")) .width(30) .height(160) .draggable(false) .gesture( PanGesture({ fingers: CONFIGURATION.PAN_GESTURE_FINGERS, distance: CONFIGURATION.PAN_GESTURE_DISTANCE }) .onActionStart(() => { this.dragRefOffset = CONFIGURATION.INIT_VALUE; }) .onActionUpdate((event: GestureEvent) => { // 通过监听GestureEvent事件,实时监听图标拖动距离 this.dragRefOffset = event.offsetX; this.leftImageWidth = this.imageWidth + this.dragRefOffset; this.rightImageWidth = CONFIGURATION.IMAGE_FULL_SIZE - this.leftImageWidth; if (this.leftImageWidth >= CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE) { // 当leftImageWidth大于等于310vp时,设置左右Image为固定值,实现停止滑动效果。 this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE; this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_RIGHT_LIMIT_SIZE; } else if (this.leftImageWidth <= CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE) { // 当leftImageWidth小于等于30vp时,设置左右Image为固定值,实现停止滑动效果。 this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE; this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_LEFT_LIMIT_SIZE; } }) .onActionEnd(() => { if (this.leftImageWidth <= CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE) { this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE; this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_LEFT_LIMIT_SIZE; this.imageWidth = CONFIGURATION.LEFT_IMAGE_LEFT_LIMIT_SIZE; } else if (this.leftImageWidth >= CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE) { this.leftImageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE; this.rightImageWidth = CONFIGURATION.RIGHT_IMAGE_RIGHT_LIMIT_SIZE; this.imageWidth = CONFIGURATION.LEFT_IMAGE_RIGHT_LIMIT_SIZE; } else { this.leftImageWidth = this.imageWidth + this.dragRefOffset; // 滑动结束时leftImageWidth等于左边原有Width+拖动距离。 this.rightImageWidth = CONFIGURATION.IMAGE_FULL_SIZE - this.leftImageWidth; // 滑动结束时rightImageWidth等于340-leftImageWidth。 this.imageWidth = this.leftImageWidth; // 滑动结束时ImageWidth等于leftImageWidth。 } }) ) ``` ## 图片切割九宫格 ### 使用说明 通过图库选择一张图片,将其切割成九宫格展示,然后可保存到图库中 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E5%9B%BE%E7%89%87%E5%88%87%E5%89%B2%E4%B9%9D%E5%AE%AB%E6%A0%BC.gif) ### 核心代码文件路径 image_operation/src/main/ets/edit/SplitImage.ets ### 实现思路 根据图片宽高信息以及要分割的行数列数,计算出每张图片的左上角起始位置的坐标及宽高,根据这些信息获取对应的pixelMap ``` export async function splitImage(l = 3, c = 3): Promise { // 选择图片 let uris = await selectImages(1) let originUri = '' // 存储切割图片 let imagePixels: image.PixelMap[] = [] if (uris && uris.length) { originUri = uris[0] // 创建图像编码ImagePacker对象 let imagePickerApi = image.createImagePacker(); // 以只读方式打开指定下标图片 let file: fileIo.File = await fileIo.open(uris[0], fileIo.OpenMode.READ_ONLY) let fd: number = file.fd; // 获取图片源 let imageSource = image.createImageSource(fd); // 图片信息 let imageInfo = await imageSource.getImageInfo(); // 图片高度除以3,就是把图片切为3份 let height = imageInfo.size.height / l; let width = imageInfo.size.width / c; // 切换为 3x3 张图片 for (let i = 0; i < l; i++) { for (let j = 0; j < c; j++) { // 设置解码参数DecodingOptions,解码获取pixelMap图片对象 let decodingOptions: image.DecodingOptions = { desiredRegion: { size: { height: height, // 切开图片高度 width: width // 切开图片宽度 }, x: j * width, // 切开x起始位置 y: i * height // 切开y起始位置 } } // 根据参数重新九宫格图片 let img: image.PixelMap = await imageSource.createPixelMap(decodingOptions); // 把生成新图片放到内存里 imagePixels.push(img); } } imagePickerApi.release(); fileIo.closeSync(fd); } return { originUri, splitImages: imagePixels } } ``` ## 两张图片拼接 ### 使用说明 通过图库选择一张图片,可横向拼接成一张图,也可竖向拼接成一张图。然后保存到图库 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E4%B8%A4%E5%BC%A0%E5%9B%BE%E7%89%87%E6%8B%BC%E6%8E%A5.gif) ### 核心代码文件路径 image_operation/src/main/ets/edit/JoinImages.ets ### 实现思路 获取要拼接图片的信息,计算出拼接后图片的大小等信息,根据这些信息创建出一个pixelMap。然后将要拼接的图片的pixelMap写入创建的空的pixelMap即可 ``` export async function joinImages(images: Array, isH = true) { try { if (images.length < 2) { return; } 获取图片信息 ... 根据要拼接图片创建拼接后的pixelMap const combineColor = new ArrayBuffer(combineOpts.size.width * combineOpts.size.height * 4); const newPixelMap = await image.createPixelMap(combineColor, combineOpts); for (let x = 0; x < images.length; x++) { const singleOpts = x === 0 ? singleOpts1 : singleOpts2 let singleColor = new ArrayBuffer(singleOpts.desiredSize!.width * singleOpts.desiredSize!.height * 4); let imageSource = x === 0 ? imageSource1 : imageSource2 let singleWidth = x === 0 ? singleWidth1 : singleWidth2 let singleHeight = x === 0 ? singleHeight1 : singleHeight2 //读取小图 const singlePixelMap = await imageSource.createPixelMap(singleOpts); await singlePixelMap.readPixelsToBuffer(singleColor); //写入大图 let area: image.PositionArea = { pixels: singleColor, offset: 0, stride: singleWidth * 4, region: { size: { height: singleHeight, width: singleWidth }, x: isH ? x === 0 ? 0 : singleWidth1 : 0, y: isH ? 0 : x === 0 ? 0 : singleHeight1 } } await newPixelMap.writePixels(area); } return newPixelMap; } catch (err) { hilog.error(0x0000, 'JOIN_IMAGES', 'PictureJoinTogether join error: ' + JSON.stringify(err)) } return } ``` ## AI抠图 ### 使用说明 长按需要被抠图的元素并拖拽 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_AI%E6%8A%A0%E5%9B%BE.gif) ### 实现思路 将Image接口的enableAnalyzer属性设为true ``` Image(this.imagePixelMap) .enableAnalyzer(true) .width('100%') ``` ## 图片加水印 ### 使用说明 从图库中选取图片,点击添加水印按钮。即可添加上水印 ### 效果预览 ![输入图片说明](screenshots/%E5%9B%BE%E7%89%87%E6%93%8D%E4%BD%9C_%E5%9B%BE%E7%89%87%E5%8A%A0%E6%B0%B4%E5%8D%B0.gif) ### 核心代码文件路径 entry/src/main/ets/views/ImageWaterMarkerView.ets ### 实现思路 1.根据canvas容器实际大小以及图片的实际大小,将选择的图片绘制到canvas中 ``` // 记录图片实际大小 let imageSource: image.ImageSource = image.createImageSource(imageInfo); imageSource.getImageInfo((err, value) => { if (err) { return; } this.hValue = Math.round(value.size.height * 1); this.wValue = Math.round(value.size.width * 1); let defaultSize: image.Size = { height: this.hValue, width: this.wValue }; let opts: image.DecodingOptions = { editable: true, desiredSize: defaultSize }; imageSource.createPixelMap(opts, (err, pixelMap) => { if (err) { return } // 将图片绘制canvas上 let rect = this.getComponentRect("imageContainer") as Record this.imageScale = (rect.right - rect.left) / this.wValue this.imageHeight = this.hValue * this.imageScale this.context.transform(this.imageScale, 0, 0, this.imageScale, 0, 0) this.pixelMap = pixelMap this.context.drawImage(this.pixelMap, 0, 0) }) }) ``` 2.在画布上绘制水印内容 ``` this.context.beginPath() this.context.font = `宋体 ${100 / this.imageScale}px}` this.context.textBaseline = "top" this.context.fillStyle = "#80b2bec3" this.context.rotate(Math.PI / 180 * 30) this.context.fillText("水印水印水印水印", 100 / this.imageScale, 100 / this.imageScale) this.context.rotate(-Math.PI / 180 * 30) this.context.closePath() ``` 3.根据图片实际大小将加水印的canvas重新绘制一遍,然后将绘制后的pixelMap保存到土库中 ``` let imageInfo = await this.pixelMap.getImageInfo() let offCanvas = new OffscreenCanvas(px2vp(imageInfo.size.width), px2vp(imageInfo.size.height)) let offContext = offCanvas.getContext("2d") let contextPixelMap = this.context.getPixelMap(0, 0, this.context.width, this.context.height) offContext.drawImage(contextPixelMap, 0, 0, offCanvas.width, offCanvas.height) savePixelMapToGalleryBySaveButton(this, offContext.getPixelMap(0, 0, offCanvas.width, offCanvas.height)); ``` ### 工程目录 ``` entry/src/main/ets/ |---common/constant | |---Constants.ets // 常量 |---entryability | |---EntryAbility.ets |---entrybackupability | |---EntryBackupAbility.ets |---pages | |---Index.ets // 主页 |---utils | |---AdjustUtil.ets // 图片调整工具类 | |---FilterUtil.ets // 图片滤镜工具类 | |---LoggerUtil.ets // 日志工具类 |---viewModel | |---IconListViewModel.ets // 操作图标视图模型 | |---MessageItem.ets // 多线程信息模型 | |---OptionViewModel.ets // 滤镜类型以及底部页签类型 |---views | |---AdjustContentView.ets // 色域调节 | |---DragToSwitchPicturesView.ets // 场景变化前后对比视图 | |---ImageEdit.ets // 图片编辑视图 | |---ImageEnableAnalyzer.ets // AI抠图视图 | |---ImagePreview.ets // 图片预览视图 | |---ImageWaterMarkerView.ets // 图片加水印视图 | |---JoinImagesView.ets // 图片拼接视图 | |---SplitImageView.ets // 图片分割视图 |---workers | |---AdjustBrightnessWork.ets // 图片亮度调整worker文件 | |---AdjustSaturationWork.ets // 图片饱和度调整worker文件 image_operation/src/main/ets/ |---components | |---ImageViewerComponent.ets // 图片预览组件 |---edit | |---CompressImage.ets // 图片压缩方法 | |---CropImage.ets // 图片裁剪方法 | |---Index.ets // 图片旋转、翻转等方法 | |---JoinImages.ets // 图片拼接方法 | |---SplitImage.ets // 图片分割方法 |---model | |---ImageModel.ets // 图片信息模型 |---readAndWrite | |---Index.ets // 图片读写、编码等方法 ```