HarmonyOS APP实战-画图APP - 第12篇:离线图像处理与保存

发布时间:2026/7/14 0:59:27
HarmonyOS APP实战-画图APP - 第12篇:离线图像处理与保存 HarmonyOS APP实战-画图APP - 第12篇导出与加载图片文件1. 开篇上一篇我们完成了操作历史管理模块实现了基于命令模式的撤销/重做栈HistoryManager类维护了两个StackCommand实例每次绘制操作封装为Command对象入栈撤销时从 undoStack 弹出并执行undo()重做时从 redoStack 弹出并执行redo()。画布状态通过快照方式保存用户可无限撤销受内存限制。至此APP 的交互逻辑链已闭环。然而画作还停留在内存中无法持久化保存或分享。本篇聚焦文件 IO 模块实现两个核心功能将画布内容导出为 PNG/JPEG 图片文件从相册加载图片作为画布背景。使用ohos.graphics.drawing提供的 PixelMap 转换能力结合ohos.file.fs和ohos.multimedia.image完成文件操作。这将使画图 APP 真正具备创作-保存-分享的完整链条。2. 核心实现2.1 基础配置模块导入与权限声明首先在module.json5中添加文件读写权限因为要访问用户文件目录和相册// entry/src/main/module.json5{module:{requestPermissions:[{name:ohos.permission.READ_MEDIA_IMAGES,reason:从相册加载图片作为画布背景},{name:ohos.permission.WRITE_MEDIA_IMAGES,reason:将画布导出为图片保存到相册},{name:ohos.permission.READ_USER_STORAGE,reason:读取用户目录文件}],// ... 其他配置}}在工具类文件FileManager.ts中导入所需模块// src/main/ets/utils/FileManager.tsimport{image}fromkit.ImageKit;// 图像编解码import{fileIo}fromkit.CoreFileKit;// 文件 IO 操作import{common}fromkit.AbilityKit;// UIAbility contextimport{photoAccessHelper}fromkit.MediaKit;// 相册访问/** * FileManager 负责画布的导出与加载 * 提供文件保存、从相册选取图片等方法 */exportclassFileManager{privatecontext:common.Context;constructor(context:common.Context){this.contextcontext;}// ... 方法实现}关键点说明ohos.multimedia.image中的image类名在 HarmonyOS 5.0 中改为kit.ImageKit本文使用最新 API若使用旧版本路径为ohos.multimedia.image权限ohos.permission.READ_MEDIA_IMAGES和WRITE_MEDIA_IMAGES需要在requestPermissions中声明运行时还需调用requestPermission系统弹窗授权common.Context用于获取文件目录路径和图库操作上下文2.2 核心逻辑导出画布为 PNG/JPEG导出流程从 Canvas 获取PixelMap→ 编码为图像数据 → 写入文件。这里使用ohos.graphics.drawing中的Canvas方法来获取像素数据但直接获取 PixelMap 更好的方式是使用CanvasRenderingContext2D的getPixelMap()方法。为了使代码符合绘制模块 API我们采用drawing模块的Canvas来创建离屏画布然后获取其像素缓冲再通过image模块编码。以下是核心导出方法// src/main/ets/utils/FileManager.tsimport{drawing}fromkit.ArkGraphics2D;// ohos.graphics.drawing/** * 将 CanvasRenderingContext2D 的内容导出为图片文件 * param canvasCtx 画布渲染上下文 * param width 导出宽度 * param height 导出高度 * param isPng 是否导出为 PNG否则 JPEG * returns 保存的文件路径 */exportasyncfunctionexportToFile(canvasCtx:CanvasRenderingContext2D,width:number,height:number,isPng:booleantrue):Promisestring{// 1. 从 CanvasRenderingContext2D 获取 PixelMapconstpixelMap:PixelMapcanvasCtx.getPixelMap(0,0,width,height);// 2. 创建图像 packer 用于编码constpacker:image.ImagePackerimage.createImagePacker();// 3. 设置编码参数constpackOpts:image.PackingOption{format:isPng?image/png:image/jpeg,quality:95// JPEG 压缩质量PNG 忽略此参数};// 4. 编码为 ArrayBufferconstpackedData:ArrayBufferawaitpacker.packing(pixelMap,packOpts);// 5. 生成文件名consttimestamp:numberDate.now();constfileName:stringdrawing_${timestamp}.${isPng?png:jpg};// 6. 获取应用沙箱目录用户文件目录constsandboxDir:stringcanvasCtx.getContext?.()?// 实际开发中通过 FileManager 类获取路径更可靠:;constfileDir:string${getContext().cacheDir};// 使用 cacheDir 避免权限问题// 7. 写入文件constfilePath:string${fileDir}/${fileName};constfile:fileIo.FilefileIo.openSync(filePath,fileIo.OpenMode.CREATE|fileIo.OpenMode.WRITE_ONLY);fileIo.writeSync(file.fd,packedData);fileIo.closeSync(file);// 8. 将文件移动到相册可选awaitmoveToGallery(filePath,fileName,isPng);returnfilePath;}/** * 移动文件到系统相册 */asyncfunctionmoveToGallery(sourcePath:string,fileName:string,isPng:boolean):Promisevoid{consthelperphotoAccessHelper.getPhotoAccessHelper(getContext());consturiawaithelper.createAsset(photoAccessHelper.PhotoType.IMAGE,isPng?png:jpg);// 实际开发中需读取源文件并写入新 URIconsole.info(File exported to gallery:,uri);}关键点说明CanvasRenderingContext2D.getPixelMap(x, y, width, height)返回当前画布区域的像素数据该接口在 HarmonyOS 5.0 支持无需额外绘制模块image.createImagePacker()创建图像编码器packing()方法将 PixelMap 编码为指定格式的 ArrayBufferfileIo.openSync()的OpenMode枚举需正确组合CREATE不存在则创建和WRITE_ONLY只写模式保存到相册需通过photoAccessHelper.createAsset()创建新媒体文件然后写入数据本文简化为直接保存到沙箱目录2.3 完整模块加载图片作为背景从相册选取图片并设置为画布背景需要实现照片选择器和背景图绘制。这里使用photoAccessHelper打开系统图库 Picker// src/main/ets/utils/ImageIO.tsimport{photoAccessHelper}fromkit.MediaKit;import{image}fromkit.ImageKit;import{common}fromkit.AbilityKit;/** * ImageIO 负责图像加载与显示 */exportclassImageIO{privatecontext:common.Context;constructor(context:common.Context){this.contextcontext;}/** * 从相册选取一张图片返回 PixelMap * returns 选中的图片 PixelMap */asyncpickImageFromGallery():PromisePixelMap{// 1. 创建相册访问实例consthelper:photoAccessHelper.PhotoAccessHelperphotoAccessHelper.getPhotoAccessHelper(this.context);// 2. 设置选取选项只选图片单张constpickOpts:photoAccessHelper.PickerOptions{MIMEType:photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,maxSelectNumber:1,isPreview:true};// 3. 打开系统 Pickerconstresult:photoAccessHelper.PhotoSelectResultawaithelper.select(pickOpts);if(result.photoUris.length0){thrownewError(No photo selected);}constselectedUri:stringresult.photoUris[0];// 4. 通过 URI 获取文件描述符constfile:fileIo.FilefileIo.openSync(selectedUri,fileIo.OpenMode.READ_ONLY);constfd:numberfile.fd;// 5. 创建图像源并解码为 PixelMapconstimageSource:image.ImageSourceimage.createImageSource(fd);constpixelMap:PixelMapawaitimageSource.createPixelMap({desiredSize:{width:1080,height:1080},// 限制最大尺寸desiredPixelFormat:image.PixelMapFormat.RGBA_8888});// 6. 关闭文件fileIo.closeSync(file);returnpixelMap;}/** * 将 PixelMap 绘制到 Canvas * param canvasCtx 画布上下文 * param pixelMap 图片像素数据 */drawPixelMapToCanvas(canvasCtx:CanvasRenderingContext2D,pixelMap:PixelMap):void{// 清空画布canvasCtx.clearRect(0,0,canvasCtx.width,canvasCtx.height);// 绘制图片作为背景铺满画布canvasCtx.drawImage(pixelMap,0,0,canvasCtx.width,canvasCtx.height);}}// 在画布组件中使用示例// Page.etsEntryComponentstruct PaintingPage{privatefileManager:FileManagernewFileManager(getContext());privateimageIO:ImageIOnewImageIO(getContext());asyncloadBackground(){try{constpixelMapawaitthis.imageIO.pickImageFromGallery();// 假设画布上下文已获取this.canvasCtx.clearRect(0,0,this.canvasWidth,this.canvasHeight);this.canvasCtx.drawImage(pixelMap,0,0,this.canvasWidth,this.canvasHeight);// 保存操作到历史如果需要撤销加载背景操作this.historyManager.pushCommand(newLoadBackgroundCommand(pixelMap));}catch(err){console.error(Load background failed:,err);}}build(){Column(){// 顶部工具栏Row(){Button(导出).onClick(()this.fileManager.exportToFile(this.canvasCtx,this.canvasWidth,this.canvasHeight))Button(加载背景).onClick(()this.loadBackground())}// 画布组件Canvas(this.canvasCtx).width(100%).height(90%)}}}关键点说明photoAccessHelper.getPhotoAccessHelper(context)需要UIAbilityContext在组件中通过getContext()获取select()方法会打开系统图片选择界面PickerOptions中的maxSelectNumber设为 1 限制单张image.createImageSource(fd)可从文件描述符创建图像源支持常见格式PNG/JPEG/WebP 等加载背景后建议同步到历史管理以便用户撤销加载背景操作图片尺寸处理desiredSize可控制解码后 pixelMap 的分辨率避免超大图片占用过多内存2.4 导出功能的 UI 集成在顶部工具栏增加导出按钮并提供格式选择弹窗// src/main/ets/pages/PaintingPage.etsStateshowExportDialog:booleanfalse;build(){Column(){// 工具栏Row(){Button(导出).onClick((){this.showExportDialogtrue;})// 其他按钮...}.padding(10)// 画布区域Canvas(this.canvasCtx).width(100%).height(90%)// 导出格式选择弹窗if(this.showExportDialog){CustomDialog({controller:this.exportDialogCtrl,content:选择导出格式,buttons:[{text:PNG,action:()this.doExport(true)},{text:JPEG,action:()this.doExport(false)},{text:取消,action:()this.showExportDialogfalse}]})}}}asyncdoExport(isPng:boolean){try{constpathawaitthis.fileManager.exportToFile(this.canvasCtx,this.canvasWidth,this.canvasHeight,isPng);// 显示提示文件已保存到相册this.showExportDialogfalse;console.info(Export success:,path);}catch(err){console.error(Export failed:,err);// 显示错误提示}}3. 运行验证完成上述代码后运行 APP 体验导出与加载功能导出功能在画布上绘制任意内容点击顶部工具栏「导出」按钮弹出格式选择对话框。选择 PNG 或 JPEG 后APP 调用exportToFile方法将当前画布内容保存为图片。成功后在控制台输出文件路径同时系统相册中出现新图片。加载背景点击「加载背景」按钮系统弹出相册选择器。选中一张图片后图片被解码为 PixelMap 并绘制到画布铺满。此时可以继续在图片上绘制形状、文字等。交互反馈导出过程中 UI 无卡顿因为异步操作加载背景后画布立即呈现图片。若用户想撤销加载背景可以用撤销按钮回退到之前状态前提是将加载操作记录到历史栈。4. 小结与预告本篇实现了画图 APP 的关键数据持久化功能通过image模块的createImagePacker将 PixelMap 编码为 PNG/JPEG 文件利用photoAccessHelper从相册选择图片作为背景。FileManager和ImageIO两个模块构成了完整的文件 IO 层使画作能导出分享也能复用外部图像资源。至此 APP 的核心框架基本完备。下一篇「本地数据库存储与读取」将聚焦于画作元数据管理使用关系型数据库ohos.data.relationalStore或首选项ohos.data.preferences保存缩略图、名称、创建时间等信息实现画作列表页面让用户能浏览、删除和管理自己的创作。这将使 APP 从单次会话工具升级为拥有持久库的完整画板应用。