HarmonyOS APP实战-画图APP - 第12篇:离线图像处理与保存
HarmonyOS APP实战-画图APP - 第12篇:导出与加载图片文件
1. 开篇
上一篇我们完成了操作历史管理模块,实现了基于命令模式的撤销/重做栈:HistoryManager类维护了两个Stack<Command>实例,每次绘制操作封装为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}from'@kit.ImageKit';// 图像编解码import{fileIo}from'@kit.CoreFileKit';// 文件 IO 操作import{common}from'@kit.AbilityKit';// UIAbility contextimport{photoAccessHelper}from'@kit.MediaKit';// 相册访问/** * FileManager 负责画布的导出与加载 * 提供文件保存、从相册选取图片等方法 */exportclassFileManager{privatecontext:common.Context;constructor(context:common.Context){this.context=context;}// ... 方法实现}关键点说明:
@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}from'@kit.ArkGraphics2D';// @ohos.graphics.drawing/** * 将 CanvasRenderingContext2D 的内容导出为图片文件 * @param canvasCtx 画布渲染上下文 * @param width 导出宽度 * @param height 导出高度 * @param isPng 是否导出为 PNG(否则 JPEG) * @returns 保存的文件路径 */exportasyncfunctionexportToFile(canvasCtx:CanvasRenderingContext2D,width:number,height:number,isPng:boolean=true):Promise<string>{// 1. 从 CanvasRenderingContext2D 获取 PixelMapconstpixelMap:PixelMap=canvasCtx.getPixelMap(0,0,width,height);// 2. 创建图像 packer 用于编码constpacker:image.ImagePacker=image.createImagePacker();// 3. 设置编码参数constpackOpts:image.PackingOption={format:isPng?'image/png':'image/jpeg',quality:95// JPEG 压缩质量,PNG 忽略此参数};// 4. 编码为 ArrayBufferconstpackedData:ArrayBuffer=awaitpacker.packing(pixelMap,packOpts);// 5. 生成文件名consttimestamp:number=Date.now();constfileName:string=`drawing_${timestamp}.${isPng?'png':'jpg'}`;// 6. 获取应用沙箱目录(用户文件目录)constsandboxDir:string=canvasCtx.getContext?.()?// 实际开发中通过 FileManager 类获取路径更可靠:'';constfileDir:string=`${getContext().cacheDir}`;// 使用 cacheDir 避免权限问题// 7. 写入文件constfilePath:string=`${fileDir}/${fileName}`;constfile:fileIo.File=fileIo.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):Promise<void>{consthelper=photoAccessHelper.getPhotoAccessHelper(getContext());consturi=awaithelper.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}from'@kit.MediaKit';import{image}from'@kit.ImageKit';import{common}from'@kit.AbilityKit';/** * ImageIO 负责图像加载与显示 */exportclassImageIO{privatecontext:common.Context;constructor(context:common.Context){this.context=context;}/** * 从相册选取一张图片,返回 PixelMap * @returns 选中的图片 PixelMap */asyncpickImageFromGallery():Promise<PixelMap>{// 1. 创建相册访问实例consthelper:photoAccessHelper.PhotoAccessHelper=photoAccessHelper.getPhotoAccessHelper(this.context);// 2. 设置选取选项:只选图片,单张constpickOpts:photoAccessHelper.PickerOptions={MIMEType:photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,maxSelectNumber:1,isPreview:true};// 3. 打开系统 Pickerconstresult:photoAccessHelper.PhotoSelectResult=awaithelper.select(pickOpts);if(result.photoUris.length===0){thrownewError('No photo selected');}constselectedUri:string=result.photoUris[0];// 4. 通过 URI 获取文件描述符constfile:fileIo.File=fileIo.openSync(selectedUri,fileIo.OpenMode.READ_ONLY);constfd:number=file.fd;// 5. 创建图像源并解码为 PixelMapconstimageSource:image.ImageSource=image.createImageSource(fd);constpixelMap:PixelMap=awaitimageSource.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.ets@Entry@Componentstruct PaintingPage{privatefileManager:FileManager=newFileManager(getContext());privateimageIO:ImageIO=newImageIO(getContext());asyncloadBackground(){try{constpixelMap=awaitthis.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.ets@StateshowExportDialog:boolean=false;build(){Column(){// 工具栏Row(){Button('导出').onClick(()=>{this.showExportDialog=true;})// 其他按钮...}.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.showExportDialog=false}]})}}}asyncdoExport(isPng:boolean){try{constpath=awaitthis.fileManager.exportToFile(this.canvasCtx,this.canvasWidth,this.canvasHeight,isPng);// 显示提示:文件已保存到相册this.showExportDialog=false;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 从单次会话工具升级为拥有持久库的完整画板应用。
