
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第9篇批量处理与历史记录1. 开篇上一篇我们实现了图片编码与导出功能核心是通过image.createImagePacker将编辑后的PixelMap编码为JPEG/PNG格式再利用photoAccessHelper写入系统相册。代码中关键部分是ImagePacker的初始化、编码参数配置packingOptions中设置format和quality以及通过FileAsset写入沙箱文件后移动至相册的逻辑。单个图片的编解码、滤镜、裁切等基础功能已经完备但用户在处理多张图片时需要逐张重复操作——这显然不够高效。本篇将构建批量处理模块支持从相册多选图片对每张图片统一应用裁切尺寸或滤镜效果并通过栈结构记录每张图片的编辑历史实现撤销undo和重做redo操作。这不仅是用户体验的跃升也是App向生产力工具迈进的关键一步。2. 核心实现2.1 基础配置与数据模型首先定义编辑历史的栈结构以及单张图片的处理任务模型。// model/EditHistoryManager.tsimport{image}fromkit.ImageKit;/** * 编辑历史管理器基于双栈实现撤销/重做 */exportclassEditHistoryManager{privateundoStack:PixelMap[][];// 撤销栈存放每次编辑后的PixelMap快照privateredoStack:PixelMap[][];// 重做栈存放撤销时弹出的PixelMapprivatemaxHistory:number20;// 最大历史步数防止内存膨胀/** * 压入新的编辑状态 * param pixelMap 当前编辑后的PixelMap */pushState(pixelMap:PixelMap):void{// 新操作产生时清空重做栈因为新操作打破了重做链this.redoStack.length0;// 复制PixelMap存入撤销栈浅拷贝即可因为PixelMap是引用类型但后续不会被修改this.undoStack.push(pixelMap);// 限制栈深度if(this.undoStack.lengththis.maxHistory){this.undoStack.shift();}}/** * 撤销返回上一步的PixelMap并将当前状态压入重做栈 */undo(origin:PixelMap):PixelMap|undefined{if(this.undoStack.length0){returnundefined;}// 当前状态入重做栈this.redoStack.push(origin);// 弹出上一步状态returnthis.undoStack.pop();}/** * 重做返回撤销前的PixelMap并将当前状态压入撤销栈 */redo(origin:PixelMap):PixelMap|undefined{if(this.redoStack.length0){returnundefined;}// 当前状态入撤销栈this.undoStack.push(origin);// 弹出重做状态returnthis.redoStack.pop();}/** * 判断是否可以撤销 */canUndo():boolean{returnthis.undoStack.length0;}/** * 判断是否可以重做 */canRedo():boolean{returnthis.redoStack.length0;}}关键点说明双栈结构是撤销/重做的经典实现撤销栈存历史重做栈存被撤销的状态。每次新编辑操作pushState必须先清空重做栈保证编辑链路的线性——这是用户交互的常识。PixelMap是引用类型但pushState时我们直接存入引用而非深拷贝因为上一帧的PixelMap在后续编辑中不会被修改每次编辑产生新PixelMap。若需绝对安全可使用PixelMap.readPixelsToBuffer()深拷贝但会大幅增加内存本场景不必要。2.2 批量处理任务模型定义单个图片的任务状态串联解码、编辑、编码三个阶段。// model/ImageTaskModel.tsimport{image}fromkit.ImageKit;/** * 单张图片的批量处理任务 */exportinterfaceImageTask{sourceUri:string;// 原始图片URI用于展示缩略图pixelMap:image.PixelMap|null;// 当前编辑后的PixelMaporiginal:image.PixelMap|null;// 原始PixelMap用于重置history:EditHistoryManager;// 该图片独立的编辑历史管理器state:pending|processing|done|error;// 任务状态thumbnail:image.PixelMap|null;// 缩略图用于列表展示}/** * 批量处理配置所有图片共用同一个配置 */exportinterfaceBatchConfig{type:crop|filter;// 裁切参数cropX:number;cropY:number;cropWidth:number;cropHeight:number;// 滤镜参数colorMatrixcolorMatrix:number[];}关键点说明每张图片独立维护EditHistoryManager所以撤销/重做是图片粒度的不是全局的——这符合多图编辑的直觉用户选一张图操作只影响这一张的历史。BatchConfig定义了统一的编辑参数所有选中的图片将应用相同的裁切或滤镜。用户切换到不同图片时可重新修改配置再次点击“应用”即可更新当前图并推入历史。2.3 批量处理页面完整实现BatchProcessPage是核心页面包含图片选择、编辑参数调节、统一应用、撤销/重做等交互。// pages/BatchProcessPage.etsimport{image}fromkit.ImageKit;import{photoAccessHelper}fromkit.MediaLibraryKit;import{fileIo}fromkit.CoreFileKit;import{common}fromkit.AbilityKit;import{EditHistoryManager}from../model/EditHistoryManager;import{ImageTask,BatchConfig}from../model/ImageTaskModel;import{emitter}fromkit.BasicServicesKit;EntryComponentstruct BatchProcessPage{StatetaskList:ImageTask[][];StatecurrentIndex:number0;// 当前预览的图片索引StatebatchConfig:BatchConfig{type:crop,cropX:0,cropY:0,cropWidth:300,cropHeight:300,colorMatrix:[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0]};StateisProcessing:booleanfalse;privatecontextgetContext(this)ascommon.UIAbilityContext;build(){Column(){// 顶部工具栏Row(){Button(选择图片).onClick(()this.selectImages());Button(统一应用).enabled(this.taskList.length0!this.isProcessing).onClick(()this.applyBatchToAll());Button(撤销).enabled(this.canUndo()).onClick(()this.undoCurrent());Button(重做).enabled(this.canRedo()).onClick(()this.redoCurrent());Button(导出全部).onClick(()this.exportAll());}.padding(10).width(100%)// 缩略图列表横向滚动List({space:10,scroller:null}){ForEach(this.taskList,(task:ImageTask,index:number){ListItem(){Image(task.thumbnail?this.createImageData(task.thumbnail):task.sourceUri).width(60).height(60).border({width:this.currentIndexindex?2:0,color:Color.Blue}).onClick(()this.currentIndexindex)}})}.height(80).width(100%).listDirection(Axis.Horizontal)// 当前图片预览if(this.taskList.length0){Image(this.createImageData(this.taskList[this.currentIndex].pixelMap!)).width(100%).height(300).objectFit(ImageFit.Contain)}// 编辑参数配置Column(){Radio({value:crop,group:editType}).checked(this.batchConfig.typecrop)Text(裁切)Radio({value:filter,group:editType}).checked(this.batchConfig.typefilter)Text(滤镜)if(this.batchConfig.typecrop){Slider({value:this.batchConfig.cropWidth,min:50,max:800,step:10}).onChange((v){this.batchConfig.cropWidthv;})Text(裁切宽度: this.batchConfig.cropWidth)Slider({value:this.batchConfig.cropHeight,min:50,max:800,step:10}).onChange((v){this.batchConfig.cropHeightv;})Text(裁切高度: this.batchConfig.cropHeight)}else{// 灰度滤镜示例Button(应用灰度滤镜).onClick((){this.batchConfig.colorMatrix[0.33,0.59,0.11,0,0,0.33,0.59,0.11,0,0,0.33,0.59,0.11,0,0,0,0,0,1,0];this.applyBatchToCurrent();})}}}}/** * 使用PhotoViewPicker多选图片 */asyncselectImages(){try{consthelperphotoAccessHelper.getPhotoAccessHelper(this.context);consturisawaithelper.select({maxSelectCount:10,MIME:[image/jpeg,image/png]});consttasks:ImageTask[][];for(consturiofuris){// 解码为PixelMapconstsourceimage.createImageSource(uri);constpixelMapawaitsource.createPixelMap({desiredPixelFormat:image.PixelMapFormat.RGBA_8888});// 创建缩略图降低分辨率以节省内存constthumbnailawaitsource.createPixelMap({desiredSize:{width:60,height:60},desiredPixelFormat:image.PixelMapFormat.RGBA_8888});source.release();tasks.push({sourceUri:uri,pixelMap:pixelMap,original:pixelMap,history:newEditHistoryManager(),state:pending,thumbnail:thumbnail});}this.taskListtasks;this.currentIndex0;}catch(err){console.error(选择图片失败,err);}}/** * 对当前图片应用裁切 */asyncapplyCropToCurrent(){consttaskthis.taskList[this.currentIndex];if(!task.pixelMap)return;try{task.stateprocessing;// 记录当前状态到历史栈task.history.pushState(task.pixelMap);constregion:image.Region{x:this.batchConfig.cropX,y:this.batchConfig.cropY,width:this.batchConfig.cropWidth,height:this.batchConfig.cropHeight};// 执行裁切task.pixelMap.crop(region);task.statedone;this.taskList.splice(this.currentIndex,1,{...task});// 触发UI刷新}catch(err){task.stateerror;console.error(裁切失败,err);}}/** * 对所有图片应用统一编辑 */asyncapplyBatchToAll(){this.isProcessingtrue;for(leti0;ithis.taskList.length;i){this.currentIndexi;if(this.batchConfig.typecrop){awaitthis.applyCropToCurrent();}else{// 滤镜应用逻辑复用第6篇的colorMatrix设置awaitthis.applyFilterToCurrent();}}this.isProcessingfalse;}asyncapplyFilterToCurrent(){consttaskthis.taskList[this.currentIndex];if(!task.pixelMap)return;task.stateprocessing;task.history.pushState(task.pixelMap);// 使用colorMatrix设置滤镜task.pixelMap.setColorMatrix(this.batchConfig.colorMatrix);task.statedone;this.taskList.splice(this.currentIndex,1,{...task});}// 撤销/重做undoCurrent(){consttaskthis.taskList[this.currentIndex];if(!task.pixelMap)return;constprevtask.history.undo(task.pixelMap);if(prev){task.pixelMapprev;this.taskList.splice(this.currentIndex,1,{...task});}}redoCurrent(){consttaskthis.taskList[this.currentIndex];if(!task.pixelMap)return;constnexttask.history.redo(task.pixelMap);if(next){task.pixelMapnext;this.taskList.splice(this.currentIndex,1,{...task});}}canUndo():boolean{returnthis.taskList[this.currentIndex]?.history.canUndo()??false;}canRedo():boolean{returnthis.taskList[this.currentIndex]?.history.canRedo()??false;}/** * PixelMap转Image组件可用的dataUrl简化展示 * 生产环境可使用packToFile后再读取此处仅示范 */createImageData(pixelMap:image.PixelMap):ResourceStr{// 简化处理实际项目中应调用imagePacker打包为Buffer再构造base64returnpixelMap;// 仅示意不可运行真实场景需编码}}关键点说明photoAccessHelper.select方法支持maxSelectCount参数实现多选这里限制最多10张。每张图片创建两个PixelMap完整分辨率用于编辑缩略图60x60用于列表展示大幅降低UI渲染压力。applyBatchToAll对每张图片依次调用裁切或滤镜会触发pushState记录历史。用户可在任意时刻对某张图片单独点“撤销”。pixelMap.crop(region)和pixelMap.setColorMatrix(matrix)是Image Kit文档提供的接口与之前章节一致。3. 运行验证在DevEco Studio中运行App进入“批量处理”功能点击“选择图片”从相册中选择2~3张图片。缩略图列表出现点击某张缩略图主预览区显示该图片。选中“裁切”拖动滑块调整裁切尺寸如300x300点击“统一应用”。所有图片依次被裁切缩略图更新。切换预览可看到每张图片都应用了相同裁切。点击“撤销”当前预览图片恢复裁切前的状态再点“重做”裁切效果恢复。切换到另一张图片点击“撤销”恢复的是该图片的历史互不干扰。4. 小结与预告本篇实现了批量处理模块的核心功能多图选择与统一编辑裁切/滤镜每张图片独立维护EditHistoryManager双栈历史支持图片粒度的撤销/重做这是App从“单张编辑”走向“批量处理”的重要升级后续可扩展为支持不同图片用不同配置或增加进度条显示批量处理进度。下一篇「水印与文字叠加」将聚焦于Canvas绘制文字或图标到PixelMap上实现可拖动、可调透明度的水印功能让你的图片具备版权标识能力。