一、写在前面

欢迎加入鸿蒙 PC 开发者社区,共同打造开发者工具生态:鸿蒙 PC 开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Pinta

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

这篇文章记录的是 Pinta 这个 .NET/GTK 桌面位图编辑器适配 HarmonyOS PC 的过程。

Pinta 是一个开源的位图绘图/图像编辑软件,定位类似 Paint.NET:画笔、橡皮、填充、选区、图层、调整、滤镜,是日常修图和涂画的轻量工具。它的技术栈很"桌面 Linux":用 C#(.NET 10) 写,界面是 GTK4 + libadwaita(通过 GirCore 绑定),成像引擎是 Cairo,插件系统是 Mono.Addins。整个工程有四百多个 .cs 文件,UI 和算法分布在 PintaPinta.CorePinta.ToolsPinta.Effects 等多个类库里。

这类项目适配鸿蒙很有代表性,也很"硬":它既不是 Electron,也不是现代 Web 工程,而是一个深度依赖 GTK4 桌面体系 + Cairo 原生成像 + .NET 运行时的程序。直接把这套搬到鸿蒙端,几乎是不可能的——后面会讲为什么。

所以这次没有选择"硬迁 GTK",而是采用更能落地的路线:

把 Pinta 的核心体验用 Web 重新实现,再用 HarmonyOS ArkWeb 包一层,形成可以构建 HAP 的鸿蒙 PC 应用;C# 原算法只作为"规格说明"参考,逐个忠实移植到 JS。

最终适配完成后,项目新增了一个零构建的 Web 前端和一个 HarmonyOS ArkWeb 工程。Web 侧负责画布渲染、图层、工具、调整、滤镜、撤销重做、选区;鸿蒙侧负责 HAP 工程承载、本地资源加载、文件保存(系统文件管理器)和平台桥接。

在这里插入图片描述

二、原项目分析:一个深度依赖 GTK4 + Cairo 的 .NET 桌面应用

适配前先把原项目摸清楚。Pinta 是个多类库的 .NET 解决方案:

Pinta/
├── Pinta/                 主程序入口(GTK 窗口、菜单、Action)
├── Pinta.Core/            核心:文档/图层模型、算法、文件格式、历史记录
├── Pinta.Tools/           工具:画笔、选区、形状、文字、油漆桶……
├── Pinta.Effects/         调整(Adjustments) + 效果(Effects)
├── Pinta.Gui.Widgets/     GTK 控件
├── Pinta.Docking/         停靠面板
├── Directory.Build.props  TargetFramework = net10.0
└── Directory.Packages.props  GirCore.Gtk-4.0 / Adw-1 / PangoCairo

几个关键事实,决定了适配难度:

  • 语言/运行时:C# / .NET 10。
  • UI:GTK4 + libadwaita,通过 GirCore 绑定;约 90 个 widget/dialog 文件。
  • 成像引擎:Cairo,而且是深度耦合——每个图层的像素缓冲本身就是 Cairo.ImageSurfacePinta.Core/Classes/UserLayer.cs),Core/Tools/Effects 里有上百个文件直接用 Cairo 画。
  • 像素语义:Cairo 的 ARGB32 在内存里是预乘 alpha + BGRA 字节序。这点后面移植时是个坑。
  • 工具约 20 个,调整 9 个,效果 38 个,完整的"文件/编辑/视图/图像/图层/调整/效果"菜单体系。

也就是说,Pinta 不是"纯算法 + 少量 UI",而是 GTK 界面、Cairo 成像、C# 业务三者紧密耦合的桌面程序。

在这里插入图片描述

三、适配路线判断:为什么不能直接搬 GTK,选 Web 化 + ArkWeb

Pinta 这种项目,理论上有三条路线。

第一条:直接把 GTK4 + .NET 搬到鸿蒙。 这条路基本是死路。经过对鸿蒙生态的调研:GTK4 / Pango / libadwaita 在 OpenHarmony 没有任何移植;而 .NET 跑鸿蒙的社区方案(NativeAOT 那条线)当时已归档停更、停在 .NET 9、且键鼠/窗口等基础能力未完成。换句话说,鸿蒙上既没有 GTK,也没有能跑桌面 GTK 的 .NET 运行时。谁要说能"赶紧直接把 GTK Pinta 搬上鸿蒙",那才是不现实的。

第二条:用 ArkTS + ArkUI 完整原生重写。 长期看最原生,但第一版工作量巨大:画布、图层合成、几十个滤镜、选区、撤销重做都要在 ArkUI 里重来。

第三条:Web 化 + ArkWeb 包一层。 这次选的就是它。

选它的原因很直接:

  • Pinta 的成像核心是 Cairo,而 Cairo 的 API 与 HTML5 Canvas 2D 几乎一一对应(都是 path/fill/stroke + ImageSurface 模型),换到 Web 渲染是所有方向里最顺的;
  • ArkWeb 本质是华为定制的 Chromium(Blink + V8),WebAssembly、Canvas2D、WebGL、JS↔ArkTS 桥都支持,能直接加载 HAP 内置 rawfile 资源;
  • Web 静态页面可以先在普通浏览器里调试,反馈快;
  • 文件保存这类系统能力,用 ArkTS 桥接补齐即可。

所以本次目标不是逐行迁 C#,而是把 Pinta 的 C# 算法当作"规格说明",在 Web 前端里忠实重新实现,保留 Pinta 的产品体验。

落地前还做了一个关键的性能预研(spike):用纯 JS 实现 Pinta 的 Invert / Desaturate 算子,对 12MP(4000×3000)大图测耗时——结果约 15ms,完全够用;重滤镜(如模糊)约 100ms,后续可上 WASM/WebGL。这一步排除了"Web 画布扛不住大图"这个最致命的风险,路线才算定下来。

四、第一步:用 Web 重新实现核心(忠实于 Cairo 的预乘 BGRA)

Web 前端是零构建的纯静态资源,模块化组织:

harmony-port/app/
├── index.html / styles.css
└── src/
    ├── pixels.js       RGBA(非预乘) ↔ 预乘 BGRA 转换、alpha-over
    ├── document.js     Document / Layer 模型(图层=预乘BGRA缓冲)
    ├── compositor.js   多图层合成(含混合模式)
    ├── filters.js      Invert/Desaturate(忠实移植) + BoxBlur
    ├── adjustments.js  自动色阶/黑白/亮度对比度/色调分离/棕褐
    ├── imageops.js     翻转/旋转/拼合/裁剪到选区
    ├── tools.js        画笔/铅笔/橡皮/填充/吸管/选区/缩放/抓手
    ├── history.js      撤销重做(脏矩形快照)
    ├── canvasview.js   渲染循环 + 指针输入
    ├── viewport.js     平移/缩放
    ├── platform.js     平台抽象(浏览器/ArkWeb 双跑)
    └── ui.js / app.js  菜单/面板装配 + 入口

这里有一个贯穿始终的架构决定:图层缓冲常驻 预乘 BGRA,跟 Pinta/Cairo 内存格式保持一致,只在加载图片和上屏时各转一次。spike 实测发现「RGBA ↔ 预乘 BGRA 往返」本身就要 ~57ms,比滤镜还贵,所以绝不能每帧转换。滤镜也是照 Pinta 的算子忠实移植,例如 Invert 在预乘空间下就是 A - 通道、Desaturate 用 (7471·B + 38470·G + 19595·R) >> 16 这套亮度权重。

为保证移植正确,核心逻辑都写了 node 自检(像素往返可逆、预乘合成出"紫色"、滤镜数值、选区内外、翻转旋转、撤销重做……),全部通过后再往鸿蒙搬。

五、第二步:打包成单文件,规避 rawfile 下的 ES 模块限制

开发时用 ES 模块(import/export)很舒服,但 ArkWeb 从 rawfile 加载页面时,模块脚本的来源 scheme(resource://rawfile)可能像 file:// 一样拦住 <script type="module"> 的 import 链

所以写了一个零依赖打包器 build-webapp.mjs:把 src/*.js 按依赖顺序拼成一个传统 IIFE 脚本 app.bundle.js,去掉 import/export,再配一个 index.html 用经典 <script> 引入。这样 ArkWeb 加载最稳。

这里踩过一个真实的坑:新增模块(history.js)忘了加进打包清单,结果单文件 bundle 里符号未定义、真机白屏(表现是底部"平台"显示 、所有动态逻辑不跑)。后来给打包器加了"漏文件防呆"——src/ 下任何 .js 没列进清单就直接构建失败。教训:单文件打包一定要有"漏文件"校验

六、第三步:新增 HarmonyOS ArkWeb 工程

Web 前端稳定后,新增了 HarmonyOS 工程(Stage 模型):

harmony-port/ohos-app/
├── AppScope/app.json5
├── build-profile.json5
├── entry/
│   ├── build-profile.json5 / hvigorfile.ts / oh-package.json5
│   └── src/main/
│       ├── module.json5            deviceTypes: ["2in1", "tablet", "phone"]
│       ├── ets/
│       │   ├── entryability/EntryAbility.ets
│       │   └── pages/Index.ets     ★ ArkWeb 宿主 + 原生桥
│       └── resources/
│           ├── base/...            图标/字符串/颜色/页面表
│           └── rawfile/webapp/     ★ 打包后的前端(index.html + app.bundle.js + styles.css)

ArkWeb 加载的资源放在 entry/src/main/resources/rawfile/webapp/,入口地址是 $rawfile('webapp/index.html')module.json5deviceTypes 带上了 2in1(鸿蒙 PC/笔记本形态)。

Index.ets 的核心加载逻辑:

Web({ src: $rawfile('webapp/index.html'), controller: this.controller })
  .javaScriptAccess(true)
  .domStorageAccess(true)
  .fileAccess(true)
  .imageAccess(true)
  .onControllerAttached(() => {
    // 在页面加载前注册原生桥
    this.controller.registerJavaScriptProxy(
      this.bridge, 'harmony', ['platform'], ['savePng']
    )
  })
  .width('100%').height('100%')

在这里插入图片描述

七、第四步:原生桥——点保存弹出系统文件管理器

只让页面打开还不够。图像编辑器的"保存"是核心闭环,而且用户希望能自己选保存位置,不是悄悄存进沙箱。

所以原生桥 NativeBridge 暴露给 Web 的对象名是 harmony,注册时把同步/异步能力分开:

  • platform()(同步):让前端探测自己跑在浏览器还是 ArkWeb;
  • savePng(dataUrl, fileName)异步,JS 端拿到的是 Promise):用 picker.DocumentViewPicker 弹出系统文件管理器,让用户选位置/文件名,再把 PNG 字节写入。
async savePng(dataUrl: string, fileName: string): Promise<string> {
  const b64 = dataUrl.substring(dataUrl.indexOf(',') + 1);
  const bytes = new util.Base64Helper().decodeSync(b64);
  const options = new picker.DocumentSaveOptions();
  options.newFileNames = [fileName];
  options.fileSuffixChoices = ['PNG 图片|.png'];
  const uris = await new picker.DocumentViewPicker(this.context).save(options);
  if (!uris || uris.length === 0) return 'cancelled';
  const file = fs.openSync(uris[0], fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  fs.writeSync(file.fd, bytes.buffer);
  fs.closeSync(file);
  return uris[0];
}

前端 platform.js 做了降级:在 ArkWeb 里走 window.harmony.savePng(返回 Promise),在普通浏览器里退化为 Blob + a[download] 下载。所以同一份前端浏览器/鸿蒙双跑。边界很清楚:Web 负责画布与算法,ArkTS 负责系统文件管理器,两边只传一个 dataURL 字符串。

在这里插入图片描述

八、第五步:构建 HAP 并安装真机

DevEco Studio 自带 hvigorw 和 hdc,可以全命令行完成「打包前端 → 构建 HAP → 安装 → 启动」。本机封装成了一个 deploy.sh

# deploy.sh 关键步骤
bash harmony-port/sync-webapp.sh          # 打包前端并同步进 rawfile/webapp

DEVECO=/Applications/DevEco-Studio.app/Contents
DEVECO_SDK_HOME=$DEVECO/sdk \
"$DEVECO/tools/hvigor/bin/hvigorw" \
  --mode module -p product=default assembleHap --no-daemon   # 构建 HAP

HDC=$DEVECO_SDK_HOME/default/openharmony/toolchains/hdc
"$HDC" install -r .../entry-default-signed.hap               # 安装
"$HDC" shell aa start -b com.pinta.ohos -a EntryAbility      # 启动

构建输出在 entry/build/default/outputs/default/,生成 entry-default-signed.hap。构建过程可能有 getContext 废弃、targetSdkVersion 建议等警告,不影响本次 MVP 的 HAP 产物;正式上架前再补齐签名、清理 API 警告即可。

在这里插入图片描述

九、把界面做成原版 Pinta 的样子,并补上应用图标

第一版 UI 是个极简骨架,后来按原版 Pinta 的真实菜单/工具/布局把界面重做成了桌面图像编辑器的样子:顶部菜单栏(文件/编辑/视图/图像/图层/调整/效果/帮助)+ 工具栏 + 左侧竖排工具箱 + 右侧停靠面板(图层 / 历史记录 / 调色板)+ 状态栏。注意原则是不放点了没反应的假按钮:上去的菜单项都接了真功能。

应用图标一开始是个占位的纯色方块,在桌面/标题栏上看像"没 logo"。后来用脚本生成了一个 1024×1024 的设计版图标(深色渐变底 + 红绿蓝三色叠加的绘图意象,full-bleed 由系统做圆角遮罩)。换图标后要卸载重装才能刷新系统图标缓存,之后标题栏、任务栏、应用网格都会显示新 logo。

十、适配成果与当前边界

这次适配已经完成的内容:

  • 分析原 .NET/GTK4/Cairo 项目结构与核心功能;
  • 性能 spike 验证 Web 画布可行;
  • 用 Web 忠实重新实现核心:画笔/铅笔/橡皮/填充/吸管/矩形选区/缩放/抓手;
  • 图层(增/复制/删/上下移/显隐 + 不透明度 + 混合模式);
  • 调整(自动色阶/黑白/亮度对比度/反色/色调分离/棕褐)+ 效果(模糊);
  • 图像操作(翻转/旋转/拼合/裁剪到选区)+ 选区约束的画笔/填充/滤镜;
  • 撤销重做(脏矩形快照)+ 历史面板;
  • 打包为单文件、新增 ArkWeb 工程、registerJavaScriptProxy 原生桥;
  • 保存走系统文件管理器(DocumentViewPicker);
  • 全命令行构建 HAP 并安装真机验证。

当前边界也很清楚:

  • 原版 38 个效果只实现了少数常用的,其余按"每个上去的都真能用"逐步补;
  • 形状/文字/渐变/魔棒等工具尚未实现;
  • "打开图片"目前走 Web 的文件选择,还没接系统文件管理器;
  • 重滤镜还是纯 JS,后续可上 WASM/WebGL;
  • 这是 MVP 与可迭代版本,正式上架前还要完善签名、权限说明和更多真机测试。

这正是 Web 化 MVP 路线的特点:先把核心体验跑通,再逐步增强。

十一、这次适配的几个经验

第一,GTK/.NET 桌面应用不要硬迁 UI。鸿蒙没有 GTK、也没有可用的桌面 .NET 运行时,纠结怎么搬 GTK 不如把产品核心动作抽出来用 Web 重做。

第二,算法可以"照着 C# 当规格重写"。Pinta 的滤镜是纯像素数学,照源码忠实移植到 JS 即可;而且要尊重原始像素语义(Cairo 是预乘 BGRA),否则颜色就错了。

第三,一个架构铁律:工作缓冲常驻预乘 BGRA,只在加载/上屏各转一次。RGBA↔预乘往返比滤镜本身还贵,每帧转换会卡。

第四,ArkWeb 包 rawfile 静态页面,打成单文件经典脚本最稳,能规避 ES 模块在本地 scheme 下被拦的问题;并且单文件打包一定要有"漏文件"校验。

第五,文件能力交给 ArkTS 原生桥。保存用 DocumentViewPicker 让用户选位置,体验和系统其他应用一致;桥接接口要小,Web 与 ArkTS 边界清晰。

第六,换应用图标后记得卸载重装刷新系统图标缓存,否则看着还是旧图标/空白。

十二、总结

Pinta 这次适配走的是一条适合"重型桌面 GUI 应用"的路线:

.NET/GTK4/Cairo 原项目分析
        ↓
判断 GTK 不可直迁 → 选 Web 化 + ArkWeb
        ↓
spike 验证 Web 画布性能
        ↓
用 Web 忠实重实现核心(预乘 BGRA / 图层 / 工具 / 调整 / 选区 / 撤销)
        ↓
打包单文件 → ArkWeb 加载 rawfile
        ↓
ArkTS bridge 补齐保存(系统文件管理器)
        ↓
构建 HarmonyOS PC HAP 并真机验证

它没有追求一步复刻所有 GTK 细节,而是先把最重要的"画图 + 图层 + 调整 + 选区 + 保存"链路做成鸿蒙 PC 上可运行、可构建、可继续迭代的版本。

对于 Pinta 这类深度依赖 GTK/Cairo/.NET 的桌面图像编辑器,Web 化 + ArkWeb 包壳 是一个非常适合首版 MVP 的方案:它绕开了 GTK 和 .NET 运行时这两道现实的墙,能快速验证产品体验,也为后续补全工具/效果、原生化和正式发布留下了清晰空间。

到这里,Pinta 从一个 .NET/GTK4 桌面图像编辑器,已经完成了到 HarmonyOS PC ArkWeb HAP 的第一版适配。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐