原生鸿蒙像素画板实战 09:图片导入导出

发布时间:2026/7/17 16:23:38
原生鸿蒙像素画板实战 09:图片导入导出 像素画编辑器里的图片导入导出看起来像两组文件选择按钮实际跨过了相册选择器、文件 URI、图片解码、像素格式、缩放、图层合成、临时文件、图库资产和动画编码。只要忽略其中一个环节就会出现很典型的问题导入后比例变形、透明背景变黑、JPG 显示脏色、GIF 只有第一帧或者界面提示保存成功但图库里找不到文件。bitArt 把输入和输出拆成ImageImportService.ets、EditorExportService.ets与EditorImageExportWorkflowService.ets。导入服务负责从系统选择图片并转换为编辑器像素导出服务负责格式编码和目标写入工作流服务在导出前结束笔触、强制保存项目并同步动画帧。三层合在一起才是一条完整的媒体链路。导入有两种语义新项目和新图层从新建入口导入图片用户期待原图决定画布尺寸并创建一份独立项目从已有工作区导入用户期待当前画布尺寸不变图片作为新图层加入。这两种路径不能共用同一个返回结果后再由页面猜测。importImageAsProject返回画布宽高、图层数组、像素数组、活动图层和图层序号。importImageToCanvas只返回适配当前画布后的单层像素。页面因此可以明确选择“替换项目上下文”或“追加图层”不会在导入服务里夹带界面状态。export interface ImageImportResult { layers: LayerItem[]; layersPixels: string[][]; activeLayer: string; canvasSize: number; canvasWidth: number; canvasHeight: number; layerSerial: number; } export interface ImageLayerImportResult { pixels: string[]; width: number; height: number; }这层区分还影响撤销导入为图层应作为当前项目的一次可撤销操作导入为新项目则进入项目管理和持久化流程不能把旧项目直接覆盖掉。用系统选择器取得 URI不自己扫描相册导入入口使用PhotoViewPicker限制 MIME 类型为图片并只选择一项。选择器返回的是 URI不是传统文件路径。应用不需要枚举用户图库也不用先取得整个媒体库的读取权限。static async selectImageUri(): Promisestring { const picker new photoAccessHelper.PhotoViewPicker(); const options new photoAccessHelper.PhotoSelectOptions(); options.MIMEType photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; options.maxSelectNumber 1; const result await picker.select(options); return result.photoUris.length 0 ? result.photoUris[0] : ; }取消选择和真正失败要在交互层区分。bitArt 将空 URI 返回为“未选择图片”异常返回为“导入失败”。两者都不会修改当前项目。若把取消也当成错误用户每次从选择器返回都会收到不必要的失败提示。URI 不一定能直接当路径打开媒体 URI 的提供者和授权方式可能不同。createImageSourceHandle先尝试用fileIo.open(uri, READ_ONLY)得到文件描述符再用文件描述符创建ImageSource若打开失败再让图片框架直接从 URI 创建数据源。private static async createImageSourceHandle( uri: string): PromiseImageSourceHandle { try { const file await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); return { source: image.createImageSource(file.fd), file }; } catch (_error) { return { source: image.createImageSource(uri), file: null }; } }无论哪条路径成功finally中都要释放PixelMap、ImageSource和已打开的文件。图片导入频率不算高但大图的解码缓冲很可观资源只等垃圾回收会让连续导入时的内存峰值迅速上升。保持比例再决定缩放到哪里导入为新项目时目标尺寸以原图比例为基础最长边不超过编辑器最大画布。导入到已有画布时比例同样保持但目标矩形需要同时放进当前宽度和高度然后居中写入透明画布。const scale Math.min( safeCanvasWidth / Math.max(1, sourceWidth), safeCanvasHeight / Math.max(1, sourceHeight) ); return { width: Math.max(1, Math.min( safeCanvasWidth, Math.round(sourceWidth * scale))), height: Math.max(1, Math.min( safeCanvasHeight, Math.round(sourceHeight * scale))) };如果把宽和高分别拉满非方形图片会变形。若只按宽度缩放竖图又可能超出画布。取两个比例中的较小值才能保证完整放入。居中时使用(canvas - image) / 2计算行列偏移剩余位置保持透明而不是填成白色。像素画导入还要接受一个现实普通照片缩小到 64×64 或更低时必然丢细节。bitArt 使用中心点近邻采样优势是颜色边界清晰不会引入双线性插值产生的半透明过渡代价是照片可能显得生硬。这符合像素编辑器的用途但不等同于专业的颜色量化和抖动算法。RGBA 缓冲要转换为编辑器颜色格式PixelMap.readPixelsToBuffer得到 RGBA 字节。编辑器内部使用#RRGGBB和#RRGGBBAA字符串因此导入时按目标像素映射源坐标再把四个通道转成十六进制颜色。private static rgbaToPixel(red: number, green: number, blue: number, alpha: number): string { if (alpha 8) { return PixelCanvasService.TRANSPARENT_COLOR; } if (alpha 247) { return # toHex(red) toHex(green) toHex(blue); } return # toHex(red) toHex(green) toHex(blue) toHex(alpha); }接近全透明的像素统一为透明色接近不透明的像素省略 alpha只有中间范围保留八位颜色。这样可减少大量无意义的00或FF差异也与现有画布颜色解析保持一致。导出前先结束笔触并同步项目用户在手指刚离开画布时立刻点导出触摸合并队列可能仍有最后一个移动点待处理。工作流先endStroke()再设置忙碌状态、关闭菜单并调用saveNow(true)。GIF 还要把活动帧的工作区像素同步回帧数组然后才构造导出帧。hooks.endStroke(); target.exportingImage true; target.mainMenuOpen false; await hooks.saveNow(true); const exportFrames target.exportImageFormat gif ? EditorImageExportWorkflowService.createExportFrames(target) : [];忙碌标记阻止重复点击产生两次编码。无论导出成功还是失败控制器都会返回可显示结果工作流再清除标记。实际工程中还应留意异常路径是否能保证复位避免一次失败后按钮永久失效。PNG 保留透明JPG 必须铺底PNG 可以保存 alphaJPG 没有透明通道。bitArt 创建 RGBA 缓冲时根据格式决定是否把透明像素铺到绘画区背景色上PNG 保留透明值JPG 先扁平化再通过ImagePacker以 JPEG 质量 92 写入。const rgbaBuffer EditorExportService.pixelsToRgbaBuffer( pixels, canvasWidth, canvasHeight, backgroundColor, format jpg, normalizedScale ); await packer.packToFile(pixelMap, file.fd, { format: format jpg ? image/jpeg : image/png, quality: format jpg ? 92 : 100 });若直接把透明像素的 RGB 零值交给 JPEG常见结果是黑底。铺底颜色必须来自当前项目设置而不是硬编码白色否则用户在深色绘画区预览正常导出 JPG 后却完全换了视觉效果。放大导出要坚持最近邻24×24 的像素画直接分享到社交平台常常会被预览缩得很小。bitArt 支持 1x 到 4x 导出倍率每个源像素复制到对应的方块不做平滑插值。for (let row 0; row exportHeight; row) { const sourceRow Math.floor(row / exportScale); for (let col 0; col exportWidth; col) { const sourceCol Math.floor(col / exportScale); scaledPixels[row * exportWidth col] pixels[sourceRow * canvasWidth sourceCol]; } }这能保持像素边缘锐利。导出倍率不能无限增大因为内存占用按面积增长4x 意味着像素数量变为 16 倍。对当前最大画布限制而言1 到 4 是更稳妥的范围。GIF 要同时处理时长、调色板和压缩GIF 不是把多张 PNG 串起来。bitArt 写入GIF89a头、逻辑屏幕描述、全局调色板、循环扩展和逐帧图像数据。每帧的毫秒时长转换为百分之一秒至少保留两个单位避免过短延时被播放器忽略。当所有帧的颜色不超过 256 种时可以建立精确调色板超过时切换到量化调色板再把颜色映射为索引。透明像素在当前实现中会按背景色铺底和 JPG 一样形成稳定的可播放画面。之后使用 LZW 编码并按最多 255 字节拆成数据子块。const delay Math.max(2, Math.round(frame.durationMs / 10)); writeGraphicControlExtension(bytes, delay); writeImageDescriptor(bytes, canvasWidth, canvasHeight); const indices pixelsToPaletteIndices( frame.pixels, canvasWidth, canvasHeight, backgroundColor, palette ); writeImageData(bytes, indices, 8);这里的边界要说清楚当前 GIF 编码适合像素画和有限色动画不追求照片级量化质量。复杂渐变超过 256 色后会产生色阶压缩这是格式和简化量化策略共同造成的结果。写入图库先走资产变更失败再弹系统确认导出服务先在cacheDir/exports写临时文件再把路径转换为 URI。随后尝试使用MediaAssetChangeRequest.createImageAssetRequest(context, sourceUri)和applyChanges导入图库若这条路径没有取得资产 URI再调用showAssetsCreationDialog让系统完成用户确认。const request photoAccessHelper.MediaAssetChangeRequest .createImageAssetRequest(context, sourceUri); await helper.applyChanges(request); const asset request.getAsset(); return asset.uri;当前项目的module.json5没有声明面向整个图库的写入权限保存入口配合系统SaveButton的临时授权结果处理。这个结论只对应 bitArt 当前的目标系统、API 和安全控件链路不能直接套到所有图库写入方式。若改成普通按钮、调整目标版本或更换媒体接口应重新核对华为官方的 [SaveButton 文档](https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/savebutton) 与对应 Media Library Kit API。系统返回临时授权失败时界面只提示重试不应伪报成功。文件导出适合平板和二合一设备除了写入图库bitArt 在适合桌面文件交互的布局中提供DocumentViewPicker.save。用户选择目标 URI和扩展名服务根据选择器索引或 URI 后缀确定 PNG、JPG、GIF再把临时文件复制过去。手机界面主要走图库保存平板和二合一设备则可以把作品放到明确目录。临时文件不能在复制或图库索引尚未完成时立即删除。当前实现保留在缓存目录由系统缓存策略处理。若未来主动清理也应放到目标写入成功之后并确认图库侧不再依赖源 URI。怎样验证图片链路准备四类输入最有效带透明边缘的 PNG、横向长图、纵向长图、颜色很多的照片。分别测试导入为新项目和导入到 32×64 的现有画布确认宽高比例、居中位置、透明区域和资源释放。导出时制作三帧不同颜色、不同持续时间的动画分别保存 PNG、JPG、GIF 和 4x 文件。检查结果时不要只看成功提示。PNG 要确认透明通道JPG 要确认背景色与设置一致GIF 要确认帧数、循环和节奏图库与文件选择器中都要实际打开一次。取消选择器、拒绝保存、连续点击导出和切后台也应覆盖。只有输入、编码、目标写入和失败反馈都能对上图片链路才算可靠。图片导入导出把应用状态与系统媒体能力连在了一起。把 URI 当 URI 处理、把透明与无透明格式分开、导出前同步最后一笔、图库与文件出口分别设计能避开大多数“代码返回成功但作品不对”的问题。系列最后还要回到整个工作台看看这些画布、时间轴、面板和系统入口怎样在手机横竖屏、平板和二合一设备上保持可用。

相关新闻