A
Air官方
【Air 系列 · 鸿蒙 PC 适配全记录】29 / 56
■ 应用:AirPhoto(AirPhoto)
■ 定位:GIMP 3.3 移植,专业图像编辑
■ 上游:GIMP 3.3.1 | 路线:GTK3+GDK OHOS | 版本:1.5.0
■ 开源: https://gitcode.com/Andy88/Harmony-Gimp-OHOS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917605141958365092
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-Gimp-OHOS
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirPhoto
GIMP 是 GNU Image Manipulation Program 的缩写,也是开源世界图像编辑的事实标准:图层、蒙版、选区、路径、色彩管理、可脚本化批处理,加上 GEGL 驱动的非破坏处理管线。把这样一个体量的应用带到鸿蒙 PC,意义远不止"多一个看图工具"——它检验的是鸿蒙平台能否承载重量级 GTK 桌面应用的完整运行时。
这条路线的障碍从一开始就是结构性的。GIMP 原生依赖 X11/Wayland 显示协议与 POSIX fork 插件进程模型,鸿蒙两者皆无;GTK 的 GdkX11/GdkWayland 后端在鸿蒙上没有任何可挂接的显示系统。AirPhoto 的回答是:不做功能裁剪的壳移植,而是自研 GDK OHOS 后端补齐 GTK 的显示层,再把 GIMP 3.3.1 引擎整体交叉编译进来。
当前鸿蒙工程的应用名为 AirPhoto,包名 com.airphoto.app,版本 1.5.0,deviceTypes 覆盖 phone、tablet 与 2in1,主窗口固定 auto_rotation_landscape 横屏,module.json5 注册了 general.image 类型的 sendData/viewData skills(文件管理器分享图片最多 8 张直达)与 ohos.permission.READ_PASTEBOARD。README 明确工程定位为华为 MateBook Pro 二合一(arm64-v8a)的原生图像编辑器,compatible SDK 为 6.1.0(23)。上游组件谱系完整:GIMP 3.3.1(GPL-3.0-or-later)、GTK 3(LGPL-2.1-or-later,含自研 OHOS GDK 后端)、GEGL、babl、GLib/json-glib。
▍二、先划清边界:不把编译通过当成适配完成
GIMP 的代码规模决定了"编译通过"与"可用的编辑器"之间隔着巨大距离:主程序能链接成功但启动黑屏、启动不崩但输入无效、输入通了但菜单点不动、菜单通了但导出死锁——每一层都是独立的坎。工程用 P1 至 P16 的阶段划分把这些坎逐个销项,README 记录的已验证清单是:编译打通、真机启动无崩溃、直达主界面、全窗口渲染、触摸与键盘事件链、鸿蒙原生菜单栏、中文化、文件加载/导出、critical 弹窗与闪退根治。
边界的另一面同样重要:有些"原版能力"在鸿蒙上被有意替换而非复刻。对照如下。
层次 原项目实现(Linux 桌面 GIMP) 鸿蒙侧当前方案
显示后端 GDK X11/Wayland 后端 + 系统窗口管理器 自研 GDK OHOS 后端,XComponent 提供原生窗口
事件来源 XEvent/Wayland 事件队列 XComponent NDK 回调经 airgimp_core_inject_* 注入 GTK
主菜单 GTK widget menubar ArkUI 原生菜单栏,经 getMenubarJson 对接真实 GimpAction
插件模型 fork + exec 独立插件进程 -Dplugins=disabled,GEGL 算子以 .so 模块随包部署
国际化 系统 gettext(glibc intl) libintl_real.c 真 gettext + zh_CN 翻译 .mo 随包
窗口合成 合成器管理多 toplevel GDK cairo 后备 surface 按 z 序合成,整帧 blit 进 OHNativeWindow
这张表里每一行的右侧都不是临时绕过,而是长期形态:插件进程模型不会回来(鸿蒙无多进程 exec),窗口合成也不会交给外部 WM(鸿蒙没有传统意义的 WM)。
▍三、鸿蒙版的整体架构
运行时架构分三层:ArkTS 壳负责 Ability 生命周期、原生菜单栏与系统交互;libgimpbridge.so 是只链接系统库的薄 NAPI 壳,注册 XComponent 回调;核心在 libairgimp_core.so——它在 OnSurfaceCreated 时按序 dlopen 41 个依赖(RTLD_GLOBAL),再 dlsym 13 个 core API,内部承载 GTK 主线程与 GIMP 3.3.1 的 libapp 静态库组。
text
ArkTS 壳(entry/src/main/ets)
├── Index.ets:XComponent(TEXTURE) 全窗口 + ArkUI 原生菜单栏
├── DialogHost / ArkSubDialog / SubWindowManager:弹窗子窗口宿主
└── FileManager / ShareManager:文件选择与系统分享
│ libgimpbridge.so(薄 NAPI 壳,仅链系统库)
▼ OnSurfaceCreated:按序 dlopen 41 个依赖(RTLD_GLOBAL)→ dlsym core API
libairgimp_core.so(GTK 线程宿主)
├── libgtk-3.so / libgdk-3.so(含自研 OHOS GDK 后端)
├── GIMP 3.3.1 libapp 静态库组(--start-group 24 个 .a)
├── libgimp{base,color,config,math,module,thumb,widgets}-3.0.so
└── GEGL/babl 运行时模块(gegl-op-*.so、babl-ext-*.so 随包)
主要目录如下:
text
AirPhoto/
├── AppScope/ # 应用级配置(com.airphoto.app)
├── entry/src/main/
│ ├── ets/ # ArkTS 壳:Ability、菜单、弹窗、文件
│ ├── cpp/
│ │ ├── napi_init.cpp # NAPI 入口与 XComponent 回调注册
│ │ ├── airgimp_core.cpp/.h # GTK 线程宿主与 core API
│ │ ├── gimp_app.cpp # GIMP 引擎引导
│ │ ├── elf_edit.py / patch_libs.py # ELF 原位编辑与依赖刷新
│ │ └── libs/arm64-v8a/ # 链接用预编译 .so(manifest 幂等)
│ ├── libs/arm64-v8a/ # 打包进 HAP 的运行时模块
│ └── resources/rawfile/gimp-share/ # GIMP 数据:menus/themes/zh_CN .mo
├── native/
│ ├── BUILD.md # 构建权威手册(顺序/坑位速查)
│ ├── patches/gtk3-ohos/ # GDK OHOS 后端权威副本
│ ├── patches/{gimp-ohos,gegl-ohos}/ # GIMP/GEGL 移植补丁
│ ├── scripts/ # env.sh、build_*.sh、deploy_modules.sh
│ ├── cross/ # meson cross file 与 CMake 工具链
│ ├── install/ # 交叉编译 PREFIX
│ └── sources/ # babl/gegl/gtk3/gimp-master 源码树
└── ai_log/ # P1-P16 阶段修复记录
▍四、核心适配过程
### 1. 薄壳与重核:dlopen 依赖栈的装配顺序
napi_init.cpp 只做一件事:把 XComponent 的 surface 与输入回调转给 core。真正的装配发生在 airgimp_core.cpp:GTK 主线程启动前,按依赖顺序 dlopen 整个库栈——babl、GEGL、GLib 系、gtk3/gdk3,全部 RTLD_GLOBAL 保证符号互见,最后 dlsym 取出 airgimp_core_surface_created 等 13 个入口。GIMP 本体以 libapp 静态库组(24 个 .a,--start-group 链接)并进 core,加上 libgimp*-3.0.so 动态库,构成一个可独立启动的 GIMP 运行时。启动路径上还埋了 AIRGIMP-INIT 系列里程碑日志(app/app.c、app/sanity.c、app/gui/gui.c),落到 gimp_trace.txt 后按最后一个里程碑即可定位启动卡死层级——BUILD.md 的坑位速查表把"启动卡死黑屏"的诊断入口直接指向这里。
### 2. GDK OHOS 后端:没有 X11 的显示通路
native/patches/gtk3-ohos/ 是整个移植的核心资产:一套以 gdkdisplay-ohos.c、gdkwindow-ohos.c、gdkeventsource-ohos.c、gdkkeys-ohos.c 等文件构成的完整 GDK 后端,作为权威副本入库,构建时由 build_gtk3.sh rsync -a --delete 覆盖到 gtk3 源码树的 gdk/ohos/,集成点(meson_options.txt 的 ohos_backend 选项、gdkdisplaymanager.c 注册 { "ohos", _gdk_ohos_display_open } 等)在 INTEGRATION.md 逐条记录。事件链路是:XComponent NDK 回调 → airgimp_core_inject_touch/key/pointer → gdk_ohos_display_inject_* → _gdk_windowing_got_event(GTK 标准注入入口);呈现链路是:GDK cairo 后端把各 toplevel 画进后备 surface,_gdk_ohos_display_present 按 z 序合成后整帧 blit 进 OHNativeWindow。窗口尺寸变化时后端主动回送 GDK_CONFIGURE,弥补没有 WM 通知的缺口。
### 3. 菜单、弹窗与文件选择的原生化
GIMP 的 GTK widget 菜单栏在 P8 阶段被整体摘除(native 侧 GIMP_OHOS_NATIVE_MENUBAR 补丁跳过创建),顶栏改由 ArkUI 原生实现:Index.ets 通过 bridge.getMenubarJson() 从 GIMP 真实的 GMenuModel(/image-menubar)拉取菜单树,解析成 MenuItemGroup 分组结构后用系统 Menu 组件渲染级联子菜单;叶子项点击经 activateMenuAction(detailedAction) 触发真实 GAction,disabled/label 变化经 menubar notify 回调实时同步到 ArkTS。文件对话框走 sentinel action 拦截:GIMP 侧无对应 action 的"分享/打开"菜单项被 activateMenu 拦截后转 PhotoViewPicker,选择结果经 airgimp_core_set_pick_handlers/complete_pick 回投 native。GIMP 自己弹出的 critical 对话框则通过 dialog_* API 家族映射到 ArkTS 子窗口(ArkSubDialog.ets/DialogHost.ets),在独立 surface 上完成注入与渲染。
### 4. 交叉编译链与 ELF 纪律
native/scripts/env.sh 定义交叉工具链与 PREFIX=native/install,构建顺序固定为 babl → gegl → gtk3(含 OHOS 后端)→ libintl(真 gettext)→ GIMP libapp → deploy_modules.sh 刷新运行时模块。工程对 ELF 后处理立了一条铁律:禁止二进制 patchelf 0.18——它会把 .dynsym/.dynamic 搬进尾端对齐 LOAD 段,鸿蒙 musl 的 reloc_all 直接崩溃(P2 阶段实证 116 个库被污染,unpatchelf.py 因此而生)。所有 NEEDED/RPATH 编辑一律走 entry/src/main/cpp/elf_edit.py 原位编辑保段布局,GIMP 安装后的动态库 NEEDED 版本号剥离(libgegl-0.4.so.0 → libgegl-0.4.so)也由它完成。meson 侧则记录了选项名漂移的教训:旧 with-*/enable-* 写法会被 meson setup --wipe ... || true 静默吞掉,改选项后必须核对 build.ninja 时间戳确认配置真正生效。
### 5. 插件内置化与中文化落地
鸿蒙无多进程 exec,GIMP 的 fork 插件模型整体失效。工程以 -Dplugins=disabled 跳过整个插件目录(gimp-ohos-build.patch 的 plugins feature),插件依赖的 bz2/lzma/poppler/archive 等以 stub 头与 .pc 放进 native/install 满足链接;功能上由 GEGL 算子补位——deploy_modules.sh 把 gegl-op-*.so、babl-ext-*.so 与 libgegl-sc-0.4.so 部署进 entry/libs/arm64-v8a 随 HAP 分发,dlopen 清单显式包含 libgegl-sc-0.4.so 防止 sanity 检查卡死。中文化是另一条完整链路:musl 下 GLib 会回退到 proxy-libintl 导致 .mo 齐备仍显示英文,工程用 build_libintl.sh 编译 libintl_real.c 真实实现覆盖 libintl.so,配合 LANGUAGE=zh_CN、GIMP3_LOCALEDIR 与 gtk_init 后重绑 gtk30 域,zh_CN/LC_MESSAGES 下 gimp30、gtk30 等 7 个域的翻译随包部署。
▍五、适配中遇到的几个难点
### 难点1:patchelf 对 ELF 段布局的破坏
现象是依赖库 dlopen 即 SIGSEGV,且涉及面极大。根因是 patchelf 0.18 修改 NEEDED 时会把 .dynsym/.dynamic 搬到文件尾端并对齐 LOAD 段,而鸿蒙 musl 加载器的 reloc_all 不接受这种布局。P2 阶段定位时已有多达 116 个库被污染。工程的处理分三层:修复存量(unpatchelf.py 还原段布局)、替代工具(elf_edit.py 原位编辑,新库名长度不得超过旧名)、流程防复发(BUILD.md 把"禁止 patchelf 0.18"写成强制条款)。这个难点的价值在于它揭示了交叉移植中一个隐形层:编译成功、链接成功都还不够,产物还必须符合目标平台加载器的布局假设。
### 难点2:hilog 流控引发的线程冻结与 ANR
现象有两级:轻则启动期日志丢失、黑屏假活(无 GTK 线程痕迹);重则导出操作后全线程零 CPU 的 ANR 循环。根因都在 hilog 的 datagram socket 流控——发送端在流控触发后会永久阻塞,任何在关键路径上调用 OH_LOG/g_print 的线程都可能被挂起;P11 阶段更进一步发现设备安全扫描会盯 *.log 后缀文件,扫描期写盘再次挂起线程,g_messages_lock 跨线程死锁由此触发。工程的解法是日志纪律的全面重构:GLib 日志与 g_printerr 一律文件优先(gimp_trace.txt、airgimp_stderr.txt),仅 ERROR 级上 hilog;应用自产诊断文件全部用 .txt 后缀;同时消除可避免的 GLib critical 源头(如 GimpProjection 的误类型转换),让"写日志"本身不再成为故障点。
### 难点3:事件注入的 NULL 节点崩溃
现象是触摸"一碰就闪退",SIGSEGV 的 si_addr=0x8。根因是事件注入路径直接把 NULL 事件节点传给 _gdk_windowing_got_event,其内部 unlink 走到 _gdk_event_queue_remove_link(display, NULL),对 GList 节点的 next 字段解引用(偏移 0x8)崩溃。修复遵循 GTK 其他后端(wayland/broadway)的既有做法:先 _gdk_event_queue_append 拿到真实节点再传入 got_event。同一阶段还修掉了触摸释放事件的顺序问题——RELEASE 事件不能先清按钮位再建事件,必须事件构造完成后再清理记账,否则 GTK 侧看到的按钮状态与事件序列矛盾,点击判定失效。这类修复的共性是:自研后端必须与 GTK 核心对事件队列的所有隐含契约对齐。
### 难点4:musl 下 gettext 的静默降级
现象是翻译 .mo 文件齐备、locale 变量正确,界面仍是英文。根因在 musl 的 libc 不提供 glibc 式 gettext,GLib 在 musl 平台回退到 proxy-libintl,g_libintl_* 全部透传空实现,翻译查找从未发生。工程用 build_libintl.sh 编译 libintl_real.c(真实 .mo 读取实现)产出 libintl.so 覆盖 proxy 版,再补三个运行时条件:LANGUAGE=zh_CN、GIMP3_LOCALEDIR 指向随包 locale 目录、gtk_init 完成后重新绑定 gtk30 域(GTK 的文本域绑定发生在初始化早期,晚到的 locale 目录必须补绑)。中文化生效的验证点也据此明确:主界面、菜单与首选项三处同时出现中文才算链路通。
### 难点5:没有窗口管理器的首帧与缩放
现象是首帧不重绘、maximize 后 UI 停在旧尺寸、欢迎对话框每次启动必弹。三个症状同源于"没有 WM":桌面 GDK 依赖合成器与 WM 发出的 configure/ frame clock 事件驱动重绘,鸿蒙侧这些信号一律缺失。工程在集成层补齐驱动:尺寸变化时主动 append GDK_CONFIGURE 并经 got_event 回送,配合 resize 计数记账;首帧与 resize 后由集成层定时器驱动 gtk_widget_queue_draw;gimprc 幂等补写 (show-welcome-dialog no) 与 (config-version) 消除欢迎弹窗的 is_update 误判。这些补齐让 GDK OHOS 后端在没有 WM 的环境里维持了与桌面一致的窗口生命周期语义。
▍六、编译、安装与启动
日常打包无需重新交叉编译依赖(预编译产物已随仓库分发),在工程根目录:
bash
./hvigorw assembleApp --no-daemon
若改动了 babl/gegl/gtk3/gimp 源码或 GDK OHOS 后端,按 native/BUILD.md 的顺序重建:
bash
source native/scripts/env.sh
native/scripts/build_babl.sh # 改 babl 时
native/scripts/build_gegl.sh # 改 gegl 时
native/scripts/build_gtk3.sh # 改 GDK OHOS 后端后必跑
native/scripts/build_libintl.sh # 改翻译链时
native/scripts/deploy_modules.sh # 重编 gegl/babl 后刷新运行时模块
./hvigorw assembleApp --no-daemon
签名产物位于:
text
entry/build/default/outputs/default/entry-default-signed.hap
连接设备后安装并启动(命令来自 native/BUILD.md 设备调试节):
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airphoto.app
可靠日志在设备侧文件而非 hilog:gimp_trace.txt(含 AIRGIMP-INIT 启动里程碑与 FATAL 崩溃栈)与 airgimp_stderr.txt。
▍七、当前功能边界
按 README 记录的 P1-P16 验证清单,当前可运行产物已覆盖:
- 完整 GIMP 3.3.1 引擎:图层、GEGL 处理管线与核心工具链在真机运行;
- 全窗口渲染:GDK OHOS 后端驱动的 cairo 合成与 XComponent 呈现,横屏全窗;
- 触摸与键盘事件链:XComponent 回调注入 GTK 主循环,画布与界面控件可交互;
- ArkUI 原生菜单栏:菜单树来自真实 GMenuModel,动作直达 GAction,级联子菜单可用;
- 中文化:GIMP 与 GTK 双域翻译生效,真 gettext 链路;
- 文件加载与导出:系统文件选择器对接,图片打开、编辑、导出全链可用;
- 系统集成:general.image 分享/查看注册(最多 8 张连送)、剪贴板读取权限。
尚未覆盖或规划中的能力:
- 原生插件体系:fork/exec 插件进程模型在鸿蒙不可用,插件以编译期内置加 GEGL 模块随包的方式替代,用户级插件安装路径不存在;
- 打印与扫描输入:依赖桌面打印栈的能力未在移植范围;
- 更广的设备矩阵:当前以 MateBook Pro 二合一(arm64-v8a)为目标形态,其他分辨率与输入组合未系统验证。
因此,当前版本更准确的定位是"跑在鸿蒙 PC 真机上的 GIMP 3.3.1 完整引擎移植版":引擎与显示、输入、菜单、文件、国际化的五条主干链路全部真实落地,剩余边界集中在插件模型与桌面外设生态。
▍八、总结
AirPhoto 证明了一条对整个 GTK 应用族群都有参考价值的路线:不裁剪引擎、不重写界面,而是把缺失的显示层以自研 GDK 后端的方式补上,让上游应用"以为自己跑在 Linux 桌面上"。薄 NAPI 壳加重型 core 的分层、以补丁副本管理的 GTK 改造、以脚本固化的构建顺序,让这条路线具备了可重复性——这正是 BUILD.md 把自己定位为"唯一权威入口"的原因。
后续工作的重心,一是把 P 阶段机制沉淀为常规回归:坑位速查表里的每一项(ELF 布局、日志纪律、事件契约、meson 选项)都值得固化为构建或校验脚本;二是跟随上游 GIMP 3.x 的演进维护补丁集,让 OHOS 后端在版本迭代中保持可 rebase。对一个以 GPL-3.0 开源的移植而言,补丁与预编译产物连同对应源码的可获得性,本身就是交付物的一部分。
■ 应用:AirPhoto(AirPhoto)
■ 定位:GIMP 3.3 移植,专业图像编辑
■ 上游:GIMP 3.3.1 | 路线:GTK3+GDK OHOS | 版本:1.5.0
■ 开源: https://gitcode.com/Andy88/Harmony-Gimp-OHOS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917605141958365092
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-Gimp-OHOS
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirPhoto
GIMP 是 GNU Image Manipulation Program 的缩写,也是开源世界图像编辑的事实标准:图层、蒙版、选区、路径、色彩管理、可脚本化批处理,加上 GEGL 驱动的非破坏处理管线。把这样一个体量的应用带到鸿蒙 PC,意义远不止"多一个看图工具"——它检验的是鸿蒙平台能否承载重量级 GTK 桌面应用的完整运行时。
这条路线的障碍从一开始就是结构性的。GIMP 原生依赖 X11/Wayland 显示协议与 POSIX fork 插件进程模型,鸿蒙两者皆无;GTK 的 GdkX11/GdkWayland 后端在鸿蒙上没有任何可挂接的显示系统。AirPhoto 的回答是:不做功能裁剪的壳移植,而是自研 GDK OHOS 后端补齐 GTK 的显示层,再把 GIMP 3.3.1 引擎整体交叉编译进来。
当前鸿蒙工程的应用名为 AirPhoto,包名 com.airphoto.app,版本 1.5.0,deviceTypes 覆盖 phone、tablet 与 2in1,主窗口固定 auto_rotation_landscape 横屏,module.json5 注册了 general.image 类型的 sendData/viewData skills(文件管理器分享图片最多 8 张直达)与 ohos.permission.READ_PASTEBOARD。README 明确工程定位为华为 MateBook Pro 二合一(arm64-v8a)的原生图像编辑器,compatible SDK 为 6.1.0(23)。上游组件谱系完整:GIMP 3.3.1(GPL-3.0-or-later)、GTK 3(LGPL-2.1-or-later,含自研 OHOS GDK 后端)、GEGL、babl、GLib/json-glib。
▍二、先划清边界:不把编译通过当成适配完成
GIMP 的代码规模决定了"编译通过"与"可用的编辑器"之间隔着巨大距离:主程序能链接成功但启动黑屏、启动不崩但输入无效、输入通了但菜单点不动、菜单通了但导出死锁——每一层都是独立的坎。工程用 P1 至 P16 的阶段划分把这些坎逐个销项,README 记录的已验证清单是:编译打通、真机启动无崩溃、直达主界面、全窗口渲染、触摸与键盘事件链、鸿蒙原生菜单栏、中文化、文件加载/导出、critical 弹窗与闪退根治。
边界的另一面同样重要:有些"原版能力"在鸿蒙上被有意替换而非复刻。对照如下。
层次 原项目实现(Linux 桌面 GIMP) 鸿蒙侧当前方案
显示后端 GDK X11/Wayland 后端 + 系统窗口管理器 自研 GDK OHOS 后端,XComponent 提供原生窗口
事件来源 XEvent/Wayland 事件队列 XComponent NDK 回调经 airgimp_core_inject_* 注入 GTK
主菜单 GTK widget menubar ArkUI 原生菜单栏,经 getMenubarJson 对接真实 GimpAction
插件模型 fork + exec 独立插件进程 -Dplugins=disabled,GEGL 算子以 .so 模块随包部署
国际化 系统 gettext(glibc intl) libintl_real.c 真 gettext + zh_CN 翻译 .mo 随包
窗口合成 合成器管理多 toplevel GDK cairo 后备 surface 按 z 序合成,整帧 blit 进 OHNativeWindow
这张表里每一行的右侧都不是临时绕过,而是长期形态:插件进程模型不会回来(鸿蒙无多进程 exec),窗口合成也不会交给外部 WM(鸿蒙没有传统意义的 WM)。
▍三、鸿蒙版的整体架构
运行时架构分三层:ArkTS 壳负责 Ability 生命周期、原生菜单栏与系统交互;libgimpbridge.so 是只链接系统库的薄 NAPI 壳,注册 XComponent 回调;核心在 libairgimp_core.so——它在 OnSurfaceCreated 时按序 dlopen 41 个依赖(RTLD_GLOBAL),再 dlsym 13 个 core API,内部承载 GTK 主线程与 GIMP 3.3.1 的 libapp 静态库组。
text
ArkTS 壳(entry/src/main/ets)
├── Index.ets:XComponent(TEXTURE) 全窗口 + ArkUI 原生菜单栏
├── DialogHost / ArkSubDialog / SubWindowManager:弹窗子窗口宿主
└── FileManager / ShareManager:文件选择与系统分享
│ libgimpbridge.so(薄 NAPI 壳,仅链系统库)
▼ OnSurfaceCreated:按序 dlopen 41 个依赖(RTLD_GLOBAL)→ dlsym core API
libairgimp_core.so(GTK 线程宿主)
├── libgtk-3.so / libgdk-3.so(含自研 OHOS GDK 后端)
├── GIMP 3.3.1 libapp 静态库组(--start-group 24 个 .a)
├── libgimp{base,color,config,math,module,thumb,widgets}-3.0.so
└── GEGL/babl 运行时模块(gegl-op-*.so、babl-ext-*.so 随包)
主要目录如下:
text
AirPhoto/
├── AppScope/ # 应用级配置(com.airphoto.app)
├── entry/src/main/
│ ├── ets/ # ArkTS 壳:Ability、菜单、弹窗、文件
│ ├── cpp/
│ │ ├── napi_init.cpp # NAPI 入口与 XComponent 回调注册
│ │ ├── airgimp_core.cpp/.h # GTK 线程宿主与 core API
│ │ ├── gimp_app.cpp # GIMP 引擎引导
│ │ ├── elf_edit.py / patch_libs.py # ELF 原位编辑与依赖刷新
│ │ └── libs/arm64-v8a/ # 链接用预编译 .so(manifest 幂等)
│ ├── libs/arm64-v8a/ # 打包进 HAP 的运行时模块
│ └── resources/rawfile/gimp-share/ # GIMP 数据:menus/themes/zh_CN .mo
├── native/
│ ├── BUILD.md # 构建权威手册(顺序/坑位速查)
│ ├── patches/gtk3-ohos/ # GDK OHOS 后端权威副本
│ ├── patches/{gimp-ohos,gegl-ohos}/ # GIMP/GEGL 移植补丁
│ ├── scripts/ # env.sh、build_*.sh、deploy_modules.sh
│ ├── cross/ # meson cross file 与 CMake 工具链
│ ├── install/ # 交叉编译 PREFIX
│ └── sources/ # babl/gegl/gtk3/gimp-master 源码树
└── ai_log/ # P1-P16 阶段修复记录
▍四、核心适配过程
### 1. 薄壳与重核:dlopen 依赖栈的装配顺序
napi_init.cpp 只做一件事:把 XComponent 的 surface 与输入回调转给 core。真正的装配发生在 airgimp_core.cpp:GTK 主线程启动前,按依赖顺序 dlopen 整个库栈——babl、GEGL、GLib 系、gtk3/gdk3,全部 RTLD_GLOBAL 保证符号互见,最后 dlsym 取出 airgimp_core_surface_created 等 13 个入口。GIMP 本体以 libapp 静态库组(24 个 .a,--start-group 链接)并进 core,加上 libgimp*-3.0.so 动态库,构成一个可独立启动的 GIMP 运行时。启动路径上还埋了 AIRGIMP-INIT 系列里程碑日志(app/app.c、app/sanity.c、app/gui/gui.c),落到 gimp_trace.txt 后按最后一个里程碑即可定位启动卡死层级——BUILD.md 的坑位速查表把"启动卡死黑屏"的诊断入口直接指向这里。
### 2. GDK OHOS 后端:没有 X11 的显示通路
native/patches/gtk3-ohos/ 是整个移植的核心资产:一套以 gdkdisplay-ohos.c、gdkwindow-ohos.c、gdkeventsource-ohos.c、gdkkeys-ohos.c 等文件构成的完整 GDK 后端,作为权威副本入库,构建时由 build_gtk3.sh rsync -a --delete 覆盖到 gtk3 源码树的 gdk/ohos/,集成点(meson_options.txt 的 ohos_backend 选项、gdkdisplaymanager.c 注册 { "ohos", _gdk_ohos_display_open } 等)在 INTEGRATION.md 逐条记录。事件链路是:XComponent NDK 回调 → airgimp_core_inject_touch/key/pointer → gdk_ohos_display_inject_* → _gdk_windowing_got_event(GTK 标准注入入口);呈现链路是:GDK cairo 后端把各 toplevel 画进后备 surface,_gdk_ohos_display_present 按 z 序合成后整帧 blit 进 OHNativeWindow。窗口尺寸变化时后端主动回送 GDK_CONFIGURE,弥补没有 WM 通知的缺口。
### 3. 菜单、弹窗与文件选择的原生化
GIMP 的 GTK widget 菜单栏在 P8 阶段被整体摘除(native 侧 GIMP_OHOS_NATIVE_MENUBAR 补丁跳过创建),顶栏改由 ArkUI 原生实现:Index.ets 通过 bridge.getMenubarJson() 从 GIMP 真实的 GMenuModel(/image-menubar)拉取菜单树,解析成 MenuItemGroup 分组结构后用系统 Menu 组件渲染级联子菜单;叶子项点击经 activateMenuAction(detailedAction) 触发真实 GAction,disabled/label 变化经 menubar notify 回调实时同步到 ArkTS。文件对话框走 sentinel action 拦截:GIMP 侧无对应 action 的"分享/打开"菜单项被 activateMenu 拦截后转 PhotoViewPicker,选择结果经 airgimp_core_set_pick_handlers/complete_pick 回投 native。GIMP 自己弹出的 critical 对话框则通过 dialog_* API 家族映射到 ArkTS 子窗口(ArkSubDialog.ets/DialogHost.ets),在独立 surface 上完成注入与渲染。
### 4. 交叉编译链与 ELF 纪律
native/scripts/env.sh 定义交叉工具链与 PREFIX=native/install,构建顺序固定为 babl → gegl → gtk3(含 OHOS 后端)→ libintl(真 gettext)→ GIMP libapp → deploy_modules.sh 刷新运行时模块。工程对 ELF 后处理立了一条铁律:禁止二进制 patchelf 0.18——它会把 .dynsym/.dynamic 搬进尾端对齐 LOAD 段,鸿蒙 musl 的 reloc_all 直接崩溃(P2 阶段实证 116 个库被污染,unpatchelf.py 因此而生)。所有 NEEDED/RPATH 编辑一律走 entry/src/main/cpp/elf_edit.py 原位编辑保段布局,GIMP 安装后的动态库 NEEDED 版本号剥离(libgegl-0.4.so.0 → libgegl-0.4.so)也由它完成。meson 侧则记录了选项名漂移的教训:旧 with-*/enable-* 写法会被 meson setup --wipe ... || true 静默吞掉,改选项后必须核对 build.ninja 时间戳确认配置真正生效。
### 5. 插件内置化与中文化落地
鸿蒙无多进程 exec,GIMP 的 fork 插件模型整体失效。工程以 -Dplugins=disabled 跳过整个插件目录(gimp-ohos-build.patch 的 plugins feature),插件依赖的 bz2/lzma/poppler/archive 等以 stub 头与 .pc 放进 native/install 满足链接;功能上由 GEGL 算子补位——deploy_modules.sh 把 gegl-op-*.so、babl-ext-*.so 与 libgegl-sc-0.4.so 部署进 entry/libs/arm64-v8a 随 HAP 分发,dlopen 清单显式包含 libgegl-sc-0.4.so 防止 sanity 检查卡死。中文化是另一条完整链路:musl 下 GLib 会回退到 proxy-libintl 导致 .mo 齐备仍显示英文,工程用 build_libintl.sh 编译 libintl_real.c 真实实现覆盖 libintl.so,配合 LANGUAGE=zh_CN、GIMP3_LOCALEDIR 与 gtk_init 后重绑 gtk30 域,zh_CN/LC_MESSAGES 下 gimp30、gtk30 等 7 个域的翻译随包部署。
▍五、适配中遇到的几个难点
### 难点1:patchelf 对 ELF 段布局的破坏
现象是依赖库 dlopen 即 SIGSEGV,且涉及面极大。根因是 patchelf 0.18 修改 NEEDED 时会把 .dynsym/.dynamic 搬到文件尾端并对齐 LOAD 段,而鸿蒙 musl 加载器的 reloc_all 不接受这种布局。P2 阶段定位时已有多达 116 个库被污染。工程的处理分三层:修复存量(unpatchelf.py 还原段布局)、替代工具(elf_edit.py 原位编辑,新库名长度不得超过旧名)、流程防复发(BUILD.md 把"禁止 patchelf 0.18"写成强制条款)。这个难点的价值在于它揭示了交叉移植中一个隐形层:编译成功、链接成功都还不够,产物还必须符合目标平台加载器的布局假设。
### 难点2:hilog 流控引发的线程冻结与 ANR
现象有两级:轻则启动期日志丢失、黑屏假活(无 GTK 线程痕迹);重则导出操作后全线程零 CPU 的 ANR 循环。根因都在 hilog 的 datagram socket 流控——发送端在流控触发后会永久阻塞,任何在关键路径上调用 OH_LOG/g_print 的线程都可能被挂起;P11 阶段更进一步发现设备安全扫描会盯 *.log 后缀文件,扫描期写盘再次挂起线程,g_messages_lock 跨线程死锁由此触发。工程的解法是日志纪律的全面重构:GLib 日志与 g_printerr 一律文件优先(gimp_trace.txt、airgimp_stderr.txt),仅 ERROR 级上 hilog;应用自产诊断文件全部用 .txt 后缀;同时消除可避免的 GLib critical 源头(如 GimpProjection 的误类型转换),让"写日志"本身不再成为故障点。
### 难点3:事件注入的 NULL 节点崩溃
现象是触摸"一碰就闪退",SIGSEGV 的 si_addr=0x8。根因是事件注入路径直接把 NULL 事件节点传给 _gdk_windowing_got_event,其内部 unlink 走到 _gdk_event_queue_remove_link(display, NULL),对 GList 节点的 next 字段解引用(偏移 0x8)崩溃。修复遵循 GTK 其他后端(wayland/broadway)的既有做法:先 _gdk_event_queue_append 拿到真实节点再传入 got_event。同一阶段还修掉了触摸释放事件的顺序问题——RELEASE 事件不能先清按钮位再建事件,必须事件构造完成后再清理记账,否则 GTK 侧看到的按钮状态与事件序列矛盾,点击判定失效。这类修复的共性是:自研后端必须与 GTK 核心对事件队列的所有隐含契约对齐。
### 难点4:musl 下 gettext 的静默降级
现象是翻译 .mo 文件齐备、locale 变量正确,界面仍是英文。根因在 musl 的 libc 不提供 glibc 式 gettext,GLib 在 musl 平台回退到 proxy-libintl,g_libintl_* 全部透传空实现,翻译查找从未发生。工程用 build_libintl.sh 编译 libintl_real.c(真实 .mo 读取实现)产出 libintl.so 覆盖 proxy 版,再补三个运行时条件:LANGUAGE=zh_CN、GIMP3_LOCALEDIR 指向随包 locale 目录、gtk_init 完成后重新绑定 gtk30 域(GTK 的文本域绑定发生在初始化早期,晚到的 locale 目录必须补绑)。中文化生效的验证点也据此明确:主界面、菜单与首选项三处同时出现中文才算链路通。
### 难点5:没有窗口管理器的首帧与缩放
现象是首帧不重绘、maximize 后 UI 停在旧尺寸、欢迎对话框每次启动必弹。三个症状同源于"没有 WM":桌面 GDK 依赖合成器与 WM 发出的 configure/ frame clock 事件驱动重绘,鸿蒙侧这些信号一律缺失。工程在集成层补齐驱动:尺寸变化时主动 append GDK_CONFIGURE 并经 got_event 回送,配合 resize 计数记账;首帧与 resize 后由集成层定时器驱动 gtk_widget_queue_draw;gimprc 幂等补写 (show-welcome-dialog no) 与 (config-version) 消除欢迎弹窗的 is_update 误判。这些补齐让 GDK OHOS 后端在没有 WM 的环境里维持了与桌面一致的窗口生命周期语义。
▍六、编译、安装与启动
日常打包无需重新交叉编译依赖(预编译产物已随仓库分发),在工程根目录:
bash
./hvigorw assembleApp --no-daemon
若改动了 babl/gegl/gtk3/gimp 源码或 GDK OHOS 后端,按 native/BUILD.md 的顺序重建:
bash
source native/scripts/env.sh
native/scripts/build_babl.sh # 改 babl 时
native/scripts/build_gegl.sh # 改 gegl 时
native/scripts/build_gtk3.sh # 改 GDK OHOS 后端后必跑
native/scripts/build_libintl.sh # 改翻译链时
native/scripts/deploy_modules.sh # 重编 gegl/babl 后刷新运行时模块
./hvigorw assembleApp --no-daemon
签名产物位于:
text
entry/build/default/outputs/default/entry-default-signed.hap
连接设备后安装并启动(命令来自 native/BUILD.md 设备调试节):
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airphoto.app
可靠日志在设备侧文件而非 hilog:gimp_trace.txt(含 AIRGIMP-INIT 启动里程碑与 FATAL 崩溃栈)与 airgimp_stderr.txt。
▍七、当前功能边界
按 README 记录的 P1-P16 验证清单,当前可运行产物已覆盖:
- 完整 GIMP 3.3.1 引擎:图层、GEGL 处理管线与核心工具链在真机运行;
- 全窗口渲染:GDK OHOS 后端驱动的 cairo 合成与 XComponent 呈现,横屏全窗;
- 触摸与键盘事件链:XComponent 回调注入 GTK 主循环,画布与界面控件可交互;
- ArkUI 原生菜单栏:菜单树来自真实 GMenuModel,动作直达 GAction,级联子菜单可用;
- 中文化:GIMP 与 GTK 双域翻译生效,真 gettext 链路;
- 文件加载与导出:系统文件选择器对接,图片打开、编辑、导出全链可用;
- 系统集成:general.image 分享/查看注册(最多 8 张连送)、剪贴板读取权限。
尚未覆盖或规划中的能力:
- 原生插件体系:fork/exec 插件进程模型在鸿蒙不可用,插件以编译期内置加 GEGL 模块随包的方式替代,用户级插件安装路径不存在;
- 打印与扫描输入:依赖桌面打印栈的能力未在移植范围;
- 更广的设备矩阵:当前以 MateBook Pro 二合一(arm64-v8a)为目标形态,其他分辨率与输入组合未系统验证。
因此,当前版本更准确的定位是"跑在鸿蒙 PC 真机上的 GIMP 3.3.1 完整引擎移植版":引擎与显示、输入、菜单、文件、国际化的五条主干链路全部真实落地,剩余边界集中在插件模型与桌面外设生态。
▍八、总结
AirPhoto 证明了一条对整个 GTK 应用族群都有参考价值的路线:不裁剪引擎、不重写界面,而是把缺失的显示层以自研 GDK 后端的方式补上,让上游应用"以为自己跑在 Linux 桌面上"。薄 NAPI 壳加重型 core 的分层、以补丁副本管理的 GTK 改造、以脚本固化的构建顺序,让这条路线具备了可重复性——这正是 BUILD.md 把自己定位为"唯一权威入口"的原因。
后续工作的重心,一是把 P 阶段机制沉淀为常规回归:坑位速查表里的每一项(ELF 布局、日志纪律、事件契约、meson 选项)都值得固化为构建或校验脚本;二是跟随上游 GIMP 3.x 的演进维护补丁集,让 OHOS 后端在版本迭代中保持可 rebase。对一个以 GPL-3.0 开源的移植而言,补丁与预编译产物连同对应源码的可获得性,本身就是交付物的一部分。
楼主
8 天前