AirPS 鸿蒙 PC 适配全记录:完整移植 PhotoGIMP 3.3.1,自研 GTK OHOS 后端承载 Photoshop 风格工作区

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

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

适配开源地址:https://gitcode.com/Andy88/Harmony-Gimp-OHOS

一、为什么要适配 AirPS

专业图像编辑器是桌面生产力工具里不可缺席的一档。修图、合成、批处理、格式转换,这些工作对图层、选区、蒙版和高位深色彩处理有硬要求,浏览器里的轻量工具替代不了。在开源世界里,GIMP 是这一档的事实标准:它的图层与选区体系完整,图像处理建立在 GEGL 图像处理图上,支持高位深与线性光工作流,格式覆盖面广。

选择 GIMP 做鸿蒙 PC 适配,难度和价值都远超一般的桌面应用移植。GIMP 3.x 是一个完整的 GTK 3 大型应用,界面、对话框、菜单、文本输入全部建在 GTK 之上;显示层直接依赖 X11/Wayland 的 GDK 后端;国际化走 gettext;插件体系依靠 fork + exec 的独立插件进程。这四条在鸿蒙上都不成立。把它带过来,等于同时回答四个问题:GTK 3 应用如何在一个没有传统显示服务器、没有窗口管理器参与应用窗口管理的系统上渲染和接收输入;没有 exec 的系统如何承载一个插件驱动的应用;Photoshop 用户的工作习惯如何低成本迁移;GPL-3.0 大型项目如何在移动操作系统上合规分发。

AirPS 的定位因此很明确:GIMP 3.3.1 引擎 + PhotoGIMP 布局配置,深度模仿 Adobe Photoshop 工作区——PS 的菜单栏结构、工具顺序、快捷键与工具手感、右侧双层面板。它与"原版 GIMP 工作区"的 AirPhoto 是同一引擎的两个发布形态:日常开发在 AirGimp 仓库进行,发布为 AirPhoto(原版 GIMP 布局)与 AirPS(Photoshop 风格布局)两个独立应用,各自有独立包名与配置取向。

当前鸿蒙工程的应用版本为 1.3.0(versionCode 1300000),包名为 com.airps.app,支持 phonetablet2in1 三种设备形态,目标 ABI 为 arm64-v8a,compatible SDK 与 target SDK 均为 6.1.0(23)。应用申请的唯一用户授权权限是剪贴板读取(READ_PASTEBOARD,用于粘贴图片入口;2in1 形态免申请),并通过 ohos.want.action.viewData / ohos.want.action.sendData 注册了 general.image 类型的图片打开与分享接收。

需要说明的是商标与合规边界:本项目与 Adobe/Photoshop、GIMP/PhotoGIMP 项目均无关联,"Photoshop 风格"仅指工作区交互布局的模仿,应用图标与启动画面为原创绘制;整个项目遵循 GPL-3.0-or-later 开源。

二、先划清边界:不是"编译通过",而是换掉显示服务与进程模型

GIMP 上游对桌面 Linux 环境的依赖是结构性的。如果只以"消除编译报错"为目标,即使把全部源码编出动态库,得到的也只是一个无法显示、无法交互的静态引擎。因此本项目的验收口径从一开始就是:真机上可安装、可启动、可完成"打开图像—编辑—导出"的完整闭环,菜单可操作,中文界面可用,对话框行为正确。

层次上游 GIMP 实现鸿蒙侧当前方案
显示服务器GDK 的 X11 / Wayland 后端自研 GDK OHOS 后端,XComponent 提供 OHNativeWindow
应用入口app/main.cmain()Stage 模型 EntryAbility + gimp_app.cpp 引导
主窗口GtkWindow + WM 窗口管理ArkTS XComponent(TEXTURE 型)+ ArkUI 原生菜单栏
对话框GtkDialog,依赖 WM 模态2in1 真子窗口 / 手机平板浮层双模式 + 自绘标题条
菜单GTK 菜单栏ArkUI 原生菜单 ⇄ 真实 GimpAction 双通道
插件模型fork + exec 独立插件进程-Dplugins=disabled,GEGL 算子以 .so 模块随包加载
国际化gettext + 系统 locale自带真 gettext 实现,.mo 随包部署
文本输入GTK IME 模块双宿主:交互留在 GTK,键盘焦点在 ArkTS
工作区布局GIMP 默认布局PhotoGIMP 配置种子(快捷键/工具手感/导出默认值)

这个边界意味着:当前 AirPS 是完整的 GIMP 3.3.1 引擎在鸿蒙上的原生运行,而不是功能子集的重新实现;但上游依赖独立插件进程的插件生态、脚本扩展等能力,按阶段保留在边界之外(详见第八节)。

三、鸿蒙版的整体架构

整体架构遵循"ArkTS 薄壳 + 原生重型核心"的双层模式。ArkTS 层只负责 Ability 生命周期、菜单栏渲染、对话框管理和输入法桥接;一个只链接系统库的薄 NAPI 桥(libgimpbridge.so)注册 XComponent 回调;真正的重量——GTK 主线程与 GIMP 引擎——全部装在 libairgimp_core.so 里,运行时按依赖拓扑顺序 dlopen 加载:

ArkTS 壳(entry/src/main/ets)
  │  napi:libgimpbridge.so(薄壳,仅链系统库,注册 XComponent 回调)
  ▼  OnSurfaceCreated 时按序 dlopen 依赖(RTLD_GLOBAL)→ dlsym 核心 API
libairgimp_core.so(GTK 线程宿主)
  ├── libgtk-3.so / libgdk-3.so(含自研 OHOS GDK 后端)
  ├── GIMP 3.3.1 libapp 静态库组(--start-group 链接)
  └── libgimp{base,color,config,math,module,thumb,widgets}-3.0.so

三条运行时链路是整个移植的骨架:

  • 渲染:GDK 的 cairo 后备 surface 按 z 序合成各 toplevel,整帧 blit 进 OHNativeWindow
  • 事件XComponent NDK 回调 → airgimp_core_inject_*gdk_ohos_display_inject_*_gdk_windowing_got_event(GTK 标准事件注入入口);
  • 菜单:ArkUI 原生菜单栏 ⇄ GIMP 真实 GimpAction,经 getMenubarJson 导出菜单树、activateMenuAction 回传执行。

版本侧的事实:GIMP 3.3.1、GTK 3.24.53、babl 0.1.118、GEGL 0.4.71、GLib 2.80.4,外加 libmypaint 1.6.1、json-c 0.17、json-glib 等依赖,全部从源码交叉编译或以预编译产物入库分发。主要目录如下:

AirPS/
├── AppScope/app.json5                  # com.airps.app,1.3.0
├── entry/src/main/
│   ├── ets/                            # Ability、菜单栏、对话框管理、输入法桥
│   ├── cpp/
│   │   ├── napi_init.cpp               # 薄 NAPI 壳:按序 dlopen 与 XComponent 回调
│   │   ├── airgimp_core.cpp            # 核心桥接
│   │   ├── gimp_app.cpp                # GIMP 引导(对齐上游 app/main.c)
│   │   ├── elf_edit.py / patch_libs.py # ELF 原位编辑工具链
│   │   └── libs/arm64-v8a/             # 链接用预编译 .so(100 余个)
│   ├── libs/arm64-v8a/                 # 随 HAP 分发的运行时模块(60 余个)
│   └── resources/rawfile/gimp-share/   # 39 MB:GIMP 数据、中文 .mo、PhotoGIMP 种子
└── native/
    ├── sources/                        # gimp-master / gtk3 / babl / gegl 源码树
    ├── patches/gtk3-ohos/              # OHOS GDK 后端权威副本
    ├── scripts/                        # env.sh 与 build_*.sh 交叉编译链
    ├── cross/                          # meson cross file 与 CMake 工具链
    └── install/                        # 交叉编译 PREFIX(入库分发)

GIMP 的数据资源(笔刷、渐变、图案、主题、菜单定义、字体配置)共约 39 MB,位于 HAP rawfile/gimp-share/。首次启动时由 EntryAbility 提取到应用沙箱,用版本戳文件保证幂等——只有版本号或构建时间变化才全量重拷。GIMP3_DATADIR 指向数据根目录而非 share 根目录,这是实测得出的细节:指错一级目录会让 g_file_get_contents 在沙箱里挂死。

四、核心工作流在真机上的验证

AirPS 的目标设备是华为 MateBook Pro(2in1)与 MatePad 平板;最近的阶段记录(P29–P32)在 HUAWEI Pura X 上补齐了手机形态的适配。下列工作流均以真机上的阶段记录为准(详见仓库 ai_log/),配图对应这些工作流。

1. 从启动直达 PS 风格主界面

应用启动后直达 GIMP 主界面:顶部是 ArkUI 渲染的深灰色(#FF323232)菜单栏,菜单顺序按 Photoshop 习惯排列——文件、编辑、选择、视图、图像、图层、颜色、工具、滤镜、窗口;左侧单色工具箱,右侧双层面板,中央画布。与 PS 一致,启动就绪后会自动弹出"新建图像"对话框,让用户第一时间开始工作。

在这里插入图片描述

2. 中文菜单不是换语言包,而是菜单双通道

菜单栏由 ArkUI 原生渲染,但每一项都对应 GIMP 内部真实的 GimpActiongetMenubarJson 把 GIMP 菜单树导出给 ArkTS,用户点击后经 activateMenuAction 回传 native 执行。这意味着菜单不是界面仿品——过滤器、色彩命令背后都是原版 GIMP 处理链。

在这里插入图片描述

3. 对话框:2in1 真子窗口与手机浮层双模式

GIMP 的图像处理几乎全部通过对话框完成(新建图像、曲线、色阶、导出……)。鸿蒙版把 GTK 对话框路由到两种承载模式:2in1 自由窗口下创建系统真子窗口,可拖拽、多实例、互不遮挡;手机和平板沉浸形态下在主窗口内以浮层卡片承载,带遮罩与居中钳制。两种模式共用同一套自绘标题条(标题 + ✕ 关闭),保证观感一致。

在这里插入图片描述

4. 触摸文本输入:键盘在 ArkTS,交互在 GTK

对话框里的数值输入框(宽度、高度、分辨率……)在纯触摸设备上支持:点按定位光标、双击全选、软键盘直挂输入、数值在会话结束时提交。输入法物理上只能由 ArkTS 组件承载焦点,因此链路是双宿主的——这部分是整个工程里打磨最久的交互,细节见第五节和第六节。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5. 打开、导出与外部图片入口

文件打开/导出闭环已在真机验证。外部图片有四条统一入口:系统分享(want)、碰一碰/隔空投送、拖放、剪贴板粘贴(含 uri 型记录),全部汇入同一条"拷入沙箱 → 待打开队列 → native 打开"链路。打开语义按画板类应用惯例分流:无文档时新建图像打开,有文档时作为图层加入当前画板。

五、核心适配过程

1. 自研 GDK OHOS 后端:替掉 X11/Wayland

这是整个移植的地基。GTK 的窗口、事件、光标、按键体系都在 GDK 层抽象,因此适配点不在 GIMP,而在给 GTK 3 写一个新后端:gdkdisplay-ohos.cgdkeventsource-ohos.cgdkdevice-ohos.cgdkkeys-ohos.cgdkcursor-ohos.c 等,在 gdkdisplaymanager.c 注册 "ohos" 显示后端。后端代码以 native/patches/gtk3-ohos/ 为权威副本入库,构建脚本在编译 GTK 前把它 rsync 进源码树——改后端只动 patches 目录,重跑 build_gtk3.sh 即可,不污染上游源码树。

渲染上,后端用 cairo 后备 surface 承接 GTK 的绘制,按 z 序合成后整帧 blit 进 OHNativeWindow;事件上,所有输入经 _gdk_windowing_got_event 这个 GTK 标准注入入口进入主循环——绕过这个入口自造事件分发,是触摸失灵类问题的常见根源。

2. 薄壳 dlopen 与打包纪律

hvigor 只编译两个 CMake 产物:薄 NAPI 桥和核心库。其余上百个预编译 .soentry/libs/arm64-v8a/ 整体打包通道,运行时由薄壳按依赖拓扑排序 dlopen——GLib 家族、Pango/Cairo、GTK、babl/GEGL、libgimp 系列,最后加载 libairgimp_core.so 并 dlsym 解析核心入口。这条纪律是踩出来的:hvigor 的链接闭包会静默丢弃它不认识的预编译库,native 库的 strip 也必须关闭,否则对齐被破坏的 ELF 在设备上 dlopen 即崩且无日志。

3. GIMP 引导与插件模型的替代

GIMP 不再经 main() 启动,由 gimp_app.cpp 对齐上游 app/main.c 的启动序列引导,i18n、配置目录、数据目录逐项初始化。插件进程模型被整体禁用(-Dplugins=disabled)——鸿蒙无多进程 exec,插件进程在设备上永远跑不起来,编译它们纯属浪费;GEGL 算子改为 .so 模块随包部署、运行时 dlopen。文件装载则补了一条进程内 GEGL 装载链兜底:按文件魔数嗅探 → GEGL load → 建图或加层,与"文件→打开"路径同构。

4. PhotoGIMP 布局的两种落地方式

PS 手感分两层落地。第一层是用户级配置种子rawfile/gimp-share/ps-seed/):PS 快捷键表(M 矩形选择、C 裁剪、W 魔棒、Alt+BackSpace 填充前景色、Ctrl+E 合并图层……)、22 个工具选项预置、滤镜与导出默认值(如 JPEG 导出质量)、PS 风格启动画面。首次启动时幂等写入用户配置目录——所有条目只在缺失时写入,不覆盖用户后续修改。第二层是 ArkTS 菜单栏的 PS 化:顶级菜单顺序重排为 PS 习惯、深灰配色、剔除与画板工作流无关的菜单项。

5. 中文化:musl 上"假成功"的 gettext

鸿蒙的 musl libc 没有 gettext。麻烦在于 glib 会回退到 proxy-libintl——所有 g_libintl_* 调用都能"成功"返回,但内容原样透传不翻译,造成".mo 文件都齐了界面还是英文"的假象。对策是自带真 gettext 实现(libintl_real.c 编译成 libintl.so 覆盖 proxy),配合 LANGUAGE=zh_CNGIMP3_LOCALEDIR 指向随包 .mo,并在 gtk_init 之后重绑 gtk30 域。随包部署 8 个翻译域:gimp30gimp30-libgimpgimp30-std-plug-insgtk30gtk30-propertiesjson-glib-1.0 等,从 GIMP 的 zh_CN.po 用 msgfmt 现编。

6. 菜单双通道与输入法桥

菜单双通道(getMenubarJson / activateMenuAction)之外,文本输入采用双宿主桥:GDK 事件钩子识别 GtkEntry 系控件的触摸命中,用 pango 自行计算光标位置;命中后通知 ArkTS 聚焦一个隐形 TextArea 拉起软键盘;键盘输入经 onChange 差分(去抖 + 硬上限)回注 GtkEditable 光标处;会话结束(点到文本框之外)时执行 gtk_spin_button_update 提交数值并收起键盘。combo 下拉则不拦截,触摸直通 GTK 原生弹层,由弹层独立承载面显示。

六、适配中遇到的几个难点

难点一:没有窗口管理器,一切"新顶层窗口"都是危险区

桌面 Linux 上对话框的位置、尺寸、模态、焦点全靠 WM 协调;鸿蒙的应用窗口体系里这些都要自理。终态方案是双模式路由:以"启动时是否处于自由多窗形态"的永续标记 ∪ 实时窗口状态为谓词,每个新对话框独立判定走真子窗口还是浮层;已开实例在形态切换后惰性不迁移(迁移等于销毁重建,闪烁且丢 GTK 状态)。真子窗口自绘 40vp 标题条,拖拽走 startMoving()(系统接管,必须在 Touch Down 回调内同步调用)+ PanGesture 兜底双通道。两个反复出现的坑:GDK 对同一对话框可能双发 show,ArkTS 侧必须幂等,否则触发重建风暴;系统 X 销毁子窗口时 GTK 模态还在,形成"窗没了、菜单锁死"的僵尸态,destroy 联动必须幂等补取消。

难点二:浮层不回写位置,就是给主窗口埋触摸死区

无 WM 下 GTK 不知道浮层被摆到了屏幕哪里。如果浮层的实际屏幕位不回写给 GDK,GTK 的窗口记账里就永远有一个挂在 root(0,0) 的隐形矩形——它会截走主窗口左上区域的触摸命中,表象是"工具箱上部点不了"。这类问题日志里一切正常,只有把"命中了哪个 widget"的探针打出来才能实锤。终态:浮层的初始布局、拖拽、键盘抬升全部经同一条 moveDialog 通道回写 GDK。

难点三:hilog 流控与安全扫描,逼出"日志一律写 .txt"的纪律

两个真机实锤的根因:其一,hilog 有流控,大量 INFO 级日志会触发停读,OH_LOG 在 datagram socket 上的发送线程被永久阻塞——表象是应用黑屏假活、零 CPU 占用、零日志;其二,设备安全服务实时扫描 *.log 文件,扫描期间写盘挂起,曾与 GLib 日志锁跨线程死锁叠加,造成导出后 ANR 循环。纪律由此固定:应用自产日志一律 .txt 后缀(gimp_trace.txt / airgimp_stderr.txt),只有 ERROR 级才上 hilog。

难点四:musl dlopen 的严格性,逼出一条 ELF 纪律

musl 的 dlopen 对 ELF 布局的要求比 glibc 严格得多。二进制 patchelf 会把 .dynsym/.dynamic 搬进尾部对齐段,污染过的库(本项目实测过 116 个)在设备上 dlopen 即崩且无日志;hvigor 的 llvm-strip 会把预编译库 64K 对齐的 PT_LOAD 重排成非法 ELF;meson 安装的库 NEEDED 带版本号后缀,而打包通道里的文件名无版本。对策是一套自研原位编辑工具链(elf_edit.py:改 NEEDED/RPATH 不动段布局)+ 关闭 native strip + NEEDED 统一去版本号。还有一个隐蔽的假绿:构建树把静态库列为 order-only 依赖,改了 .a 不会触发核心 .so 重链,hvigor 全绿但设备跑的是旧代码——改静态库后必须删产物强制重链并做字节级核验。

难点五:触摸输入的数值"复利损坏"

对话框数值框的提交时机踩过一个大坑:如果逐字符把文本提交给 GtkSpinButton,spin 控件会重写文本,与差分镜像失配,数值被反复翻倍——实测一个 1280 能一路变成 524288。纪律是值提交只发生在输入会话结束时(点到文本框之外或点击确定按钮),一次 gtk_spin_button_update 定案。同族的坑还有:同一次触摸浮层和主窗各收一份事件(需 30ms 去重);GTK3 combo 默认的菜单模式在无 WM 环境不可承载(CSS 强制列表模式);双击全选在纯触摸下没有 GTK 语义(core 侧自行记账合成)。

难点六:外部接收链路各有各的暗坑

碰一碰/隔空投送的 receive() 要求传 file:// URI 而不是裸沙箱路径(否则原生层直接返回 EINVAL 且无任何回调),且接收目录必须"已存在且为空",每次传输开独立子目录;Promise 不接 catch 则失败无声。拖放接收要注意 kit 导入在不同 SDK 版本的可用性差异;剪贴板粘贴要同时处理 pixelMap 与 uri 型两种记录,只按主 MIME 分流会静默跳过图库复制来的图片。

七、编译、安装与启动

环境前置:DevEco Studio(提供 hvigorw 与 OHOS SDK)、HarmonyOS SDK 6.1.0(23)、meson + ninja、python3、交叉工具链(clang/llvm 系列,路径见 native/scripts/env.sh)。

预编译依赖已随仓库分发,日常打包只需要:

cd AirPS
./hvigorw assembleApp --no-daemon
# 产物:entry/build/default/outputs/default/entry-default-signed.hap

当前仓库构建出的签名 HAP 约 92 MiB(含 39 MB 资源数据、60 余个运行时模块与全部依赖库)。只有在改动 babl / GEGL / GTK 后端 / GIMP 本体时才需要重新交叉编译:

source native/scripts/env.sh
native/scripts/build_babl.sh        # 改 babl
native/scripts/build_gegl.sh        # 改 GEGL
native/scripts/build_gtk3.sh        # 改 OHOS GDK 后端(patches/gtk3-ohos)
native/scripts/build_libintl.sh     # 中文化真 gettext
# GIMP 本体:native/build/gimp 构建树内 ninja 编译 libapp
native/scripts/deploy_modules.sh    # 刷新 entry/libs 运行时模块
./hvigorw assembleApp --no-daemon

完整构建顺序以 native/BUILD.md 为权威入口(文档中个别历史路径沿袭开发仓库 AirGimp 的表述,以 env.sh 当前值为准)。安装与启动:

hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airps.app

排障以文件日志为准(原因见难点三):

hdc shell "tail -30 /data/app/el2/100/base/com.airps.app/haps/entry/files/gimp_trace.txt"
hdc shell "tail -30 /data/app/el2/100/base/com.airps.app/haps/entry/files/airgimp_stderr.txt"

八、当前功能边界与合规

当前 HarmonyOS 版已在真机验证的能力:

  • 可安装的 HAP、Stage 模型 Ability、TEXTURE 型 XComponent 全窗口渲染,覆盖 phone / tablet / 2in1 三形态;
  • GIMP 3.3.1 引擎完整引导,直达主界面,启动无崩溃;
  • ArkUI 原生菜单栏(中文化、PS 化顺序)⇄ 真实 GimpAction 执行;
  • 触摸与硬件键盘事件链;GtkEntry 系触摸输入(点选光标、双击全选、软键盘直挂、数值提交);
  • 多实例对话框:2in1 真子窗口与手机/平板浮层双模式,含短屏滚动与底部手势区避让(P29–P32 手机形态专项);
  • 文件加载与导出闭环;
  • 外部图片四入口(分享 / 碰一碰 / 拖放 / 剪贴板),空文档新建、有文档加层的打开语义;
  • PhotoGIMP 工作区:PS 快捷键、工具预置、导出默认值、启动画面。

边界之外的能力要如实说明:GIMP 上游的插件进程体系被整体禁用,依赖独立插件进程的第三方扩展与脚本插件暂不在当前形态内,文件装载导出由进程内 GEGL 链兜底;手写笔压感、捏合缩放、暗黑模式等社区移植验收标准项,落地状态以仓库 ai_log/ 与 README 的阶段记录为准,不提前宣称完成。

GPL 合规是这类项目绕不开的一环:GIMP 与 GEGL 均为 GPL-3.0-or-later,本项目对源码的全部修改在同协议下公开,预编译产物与源码同仓分发以满足对应源义务;再分发 HAP 或预编译库时必须一并提供完整源码。GIMP 是 GNOME 项目商标,AirPS 是独立衍生作品,不代表 GNOME 官方背书;PhotoGIMP 布局配置(GPL-3.0)来自 Diolinux 开源项目。签名私钥与含密钥配置严禁入库,仓库提供发布前的校验命令逐项排查。

九、总结

AirPS 的适配价值,不在于"能跑一个 GTK 演示程序",而在于证明了一条把 GTK 大型桌面应用整体搬到鸿蒙的路径:用自研 GDK OHOS 后端接替 X11/Wayland,用 ArkTS 原生菜单与自绘标题条补上无 WM 的窗口治理,用真 gettext 与配置种子保住本地化与工作区手感,再用一套 ELF 原位编辑纪律守住 musl dlopen 的严格性。最终产物不是静态界面,而是能在 HarmonyOS PC、平板乃至手机上安装、启动并完成真实图像编辑工作的 GIMP 3.3.1。

这套沉淀已经在系列工程中复用:OHOS GDK 后端、对话框双模式路由、触摸文本输入终态,已成为其它 GTK/Qt 移植工程直接照抄的基准实现。后续工作的重心同样清晰:插件能力的进程内替代、验收标准项(手写笔、捏合缩放、暗黑模式)的逐项落地,以及对 GIMP 上游 3.x 后续版本的持续跟进——每一步都以可构建的 HAP 和真机上的实际编辑行为作为验收依据。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐