Air 系列应用交流/AirPS(GIMP)· 鸿蒙 PC 适配全记录

AirPS(GIMP)· 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】27 / 56

■ 应用:AirPS(AirPS)
■ 定位:专业级图像编辑套件(GIMP)
■ 上游:GIMP 3.3.1 | 路线:GTK3+GDK OHOS | 版本:1.5.0
■ 开源: https://gitcode.com/Andy88/Harmony-Gimp-OHOS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917614561109391626
■ 官网(免费下载): 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

▍一、为什么要适配 AirPS

专业图像编辑是桌面平台上对系统底层能力要求最苛刻的应用类别之一:它需要稳定的窗口合成、精确的指针与键盘事件、大内存吞吐和完整的像素格式管线。AirPS 把这个类别带到了鸿蒙上——它以 GNOME GIMP 3.3.1 为核心引擎,叠加 Diolinux/PhotoGIMP 的工作区布局配置,深度模仿 Adobe Photoshop 的使用习惯:PS 菜单栏结构、单色工具图标、PS 工具顺序与快捷键、右侧双层面板。项目文档明确了合规边界:与 Adobe/Photoshop、GIMP/PhotoGIMP 均无关联,"Photoshop 风格"仅指工作区交互布局,引擎遵循 GPL-3.0-or-later,PhotoGIMP 配置遵循 GPL-3.0。

选择 GIMP 而不是自研画布,是因为 GIMP 的价值在于二十余年积累的图层、选区、色彩管理与 GEGL 非破坏处理图,这些能力无法在短时间内复刻。真正的问题是工程性的:GIMP 原生依赖 X11/Wayland 显示后端与 POSIX fork/exec 插件模型,两者在鸿蒙上都不存在。AirPS 的答案是不改写引擎,而是把缺失的平台层补齐——自研 GDK 的 OHOS 后端替换 X11/Wayland,用内置化改造替代插件进程。

当前工程包名 com.airps.app,版本 1.5.0,deviceTypes 覆盖 phone、tablet 与 2in1,orientation 声明为 auto_rotation_landscape,目标 ABI 为 arm64-v8a。README 将目标设备定为 HUAWEI MateBook Pro(2in1)与 MatePad 系列;ai_log/ 的阶段记录显示近期的主验证设备为 MatePad Mini 悦读版,最新阶段 P41 完成于 2026-09-28。

▍二、先划清边界:不把编译通过当成适配完成

把 GIMP 编成 arm64 静态库只是起点。没有显示后端,GTK 主循环创建的窗口无处呈现;没有事件通路,界面是一张静态图;没有插件进程模型,滤镜与导入导出会大面积失效;没有真 gettext,中文化无从谈起。AirPS 把"适配完成"定义为这四层平台依赖全部被鸿蒙侧等价物替换,且替换必须落在权威补丁与脚本里可重建,而不是一次性手工产物。

层次 传统桌面 GIMP 实现 鸿蒙侧当前方案
应用入口 C main 直接进入 GTK 主线程 Stage 模型 EntryAbility + NAPI 薄壳 libgimpbridge.so
显示后端 X11/Wayland GDK 后端 + 系统窗口管理器 自研 GDK ohos 后端(native/patches/gtk3-ohos/)+ OHNativeWindow 整帧 blit
事件来源 XEvent/Wayland 事件队列 XComponent NDK 回调 → airgimp_core_inject_* → _gdk_windowing_got_event
弹窗体系 GTK 对话框由窗口管理器装饰与管理 双模式实例化:2in1 自由多窗走真子窗口,手机/平板走主窗内浮层 + 自绘标题条
插件模型 POSIX fork/exec 插件进程 -Dplugins=disabled 整体内置化,GEGL 算子以 .so 模块随包
菜单栏 GTK 菜单控件 ArkUI 原生菜单栏 ⇄ GIMP 真实 GimpAction(getMenubarJson/activateMenuAction)
国际化 系统 gettext + .mo 检索 libintl_real.c 真 gettext 替代 musl proxy-libintl + zh_CN .mo 随包

边界之外的部分也如实登记:插件进程体系整体停用后,依赖独立插件进程的能力不进入当前产物;GIMP 的窗口装饰交给 ArkTS 层的自绘标题条承担;打印等桌面外设链路未在当前阶段范围。每一条边界都能对应到构建选项、补丁目录或模块清单里的一个具体决定。

▍三、鸿蒙版的整体架构

运行时链路是一条"薄壳驱动重核"的单线程编排:ArkTS 壳注册 XComponent 回调,surface 创建时由 C++ 薄壳 libgimpbridge.so 按序 dlopen 41 个依赖库(RTLD_GLOBAL),再 dlsym 取得 13 个 core API;libairgimp_core.so 作为 GTK 线程宿主,内部承载 libgtk-3/libgdk-3(含 OHOS 后端)、GIMP 3.3.1 的 libapp 静态库组(--start-group 链接 24 个 .a)与 libgimp 系列动态库。

text
EntryAbility / Index.ets(ArkTS 壳:菜单栏、XComponent、窗口与分享管理)
├── napi_init.cpp(薄壳,仅链系统库,注册 XComponent 回调)
│ └── OnSurfaceCreated:按序 dlopen 41 个依赖(RTLD_GLOBAL)→ dlsym 13 个 core API
├── libairgimp_core.so(GTK 线程宿主)
│ ├── libgtk-3.so / libgdk-3.so(含 GDK ohos 后端)
│ ├── GIMP 3.3.1 libapp 静态库组(24 个 .a)
│ └── libgimp{base,color,config,math,module,thumb,widgets}-3.0.so
├── 事件链:XComponent → airgimp_core_inject_* → gdk_ohos_display_inject_* → _gdk_windowing_got_event
├── 呈现链:GDK cairo 后备 surface → 按 z 序合成 → 整帧 blit 进 OHNativeWindow
└── 弹窗链:GDK classify → core ops → NAPI TSFN → DialogService(子窗口 / 浮层分流)


主要目录如下:

text
AirPS/
├── AppScope/app.json5 # 包名 com.airps.app、版本 1.5.0
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/Index.ets # 壳主页面:菜单栏、XComponent、注入接线
│ │ ├── components/DialogHost.ets # 浮层弹窗宿主(主窗内实例)
│ │ ├── components/DialogTitleBar.ets # 自绘标题条(标题 + 关闭按钮)
│ │ └── utils/SubWindowManager.ets # 真子窗口宿主(自由多窗形态)
│ │ └── utils/FileManager.ets # 打开/导出文件流转
│ │ └── utils/ImmersiveManager.ets # 窗口形态判定(FLOATING/FULL_SCREEN/MAXIMIZE)
│ ├── cpp/
│ │ ├── napi_init.cpp # NAPI 注册与回调分发
│ │ ├── airgimp_core.cpp # GTK 线程宿主与 core API
│ │ ├── gimp_app.cpp # GIMP 引导
│ │ ├── elf_edit.py / patch_libs.py # ELF 原位编辑与依赖刷新
│ │ └── libs/arm64-v8a/ # 链接用预编译 .so(从 native/install 刷新)
│ ├── libs/arm64-v8a/ # 随包运行时模块(gegl-op-*、babl-ext-*、libgegl-sc)
│ └── resources/rawfile/gimp-share/ # 菜单、主题、字体、zh_CN .mo、schemas
├── native/
│ ├── BUILD.md # 全链构建手册与坑位速查
│ ├── patches/gtk3-ohos/ # GDK OHOS 后端权威副本 + INTEGRATION.md
│ ├── scripts/ # env.sh、build_babl/gegl/gtk3/gimp、deploy_modules.sh
│ ├── cross/ # meson cross file 与 CMake 工具链
│ ├── install/ # 交叉编译 PREFIX(lib/include/share)
│ └── sources/ # babl/gegl/gtk3 本地 checkout、gimp-master-ps 分叉
└── ai_log/ # P1–P41 逐阶段移植与修复记录


▍四、核心适配过程

### 1. 薄 NAPI 壳与 dlopen 依赖栈编排

ArkTS 层不做任何图像处理,只承担壳职责;entry/src/main/cpp/napi_init.cpp 保持"仅链系统库"的薄度。真正的装配发生在 OnSurfaceCreated:按依赖顺序 dlopen 41 个库并以 RTLD_GLOBAL 打开符号表,随后 dlsym 取 13 个 core API 驱动 libairgimp_core.so 启动 GTK 主线程。依赖库的 NEEDED 关系经过 patch_libs.py 与 elf_edit.py 的系统性整理——meson install 产物默认带版本号后缀,而打包集使用无版本文件名,构建脚本在安装后自动做 --replace-needed 原位改写。

### 2. GDK OHOS 后端:补上缺失的平台层

native/patches/gtk3-ohos/ 是 GDK OHOS 后端的权威副本(gdkdisplay-ohos.c、gdkwindow-ohos.c、gdkkeys-ohos.c 等),build_gtk3.sh 构建前以 rsync -a --delete 覆盖进源码树,集成点在 INTEGRATION.md 登记:meson_options.txt 增加 ohos_backend 选项、gdkdisplaymanager.c 注册 { "ohos", _gdk_ohos_display_open }。呈现路径上没有系统合成器可用,后端让每个 toplevel 维护 cairo 后备 surface,_gdk_ohos_display_present 按 z 序合成后整帧 blit 进 OHNativeWindow;无 WM 驱动 frame clock 的问题由集成层补齐(maximize 定时器与 gtk_widget_queue_draw)。

### 3. 事件注入走 GTK 标准入口

触摸与键盘事件从 XComponent NDK 回调进入后,统一经 airgimp_core_inject_*、gdk_ohos_display_inject_* 到达 _gdk_windowing_got_event 这一 GTK 标准注入入口。选择标准入口不是风格偏好:GDK 内部的事件记账(事件队列节点、设备 grab 状态)依赖该路径完整执行,绕过它会在后续 unlink 时拿到空节点直接崩溃(见难点三)。窗口尺寸语义同样需要回送 GDK_CONFIGURE,move_resize 尺寸变化时 append configure 事件才能让 maximize 后的 UI 重排。

### 4. 菜单桥接与 PhotoGIMP 工作区

GIMP 的动作体系(GimpAction)被完整保留,壳侧用 ArkUI 原生菜单栏承接视觉:core 侧 getMenubarJson 导出菜单结构,ArkUI 渲染后点击经 activateMenuAction 激活真实动作。P41 阶段还实现了二级菜单注入(文件→添加照片→本机相册,PhotoViewPicker 多选不超过 8 张,复用 drop-in→pendingOpen 管线)。PhotoGIMP 布局配置随 gimp-share 数据目录分发,工具箱图标、工具顺序与右侧面板结构对齐 PS 习惯。

### 5. 弹窗双模式实例化与自绘标题条

GDK 侧的对话框分类(type_hint=DIALOG/UTILITY 或带 transient_for 的 toplevel)被 ohos_dialog_classify 捕获后,经 core 的 DialogClaim 认领表与 NAPI TSFN 转发到 ArkTS DialogService 分流:2in1 自由多窗形态创建真子窗口(createSubWindowWithOptions,decorEnabled:false),可拖拽、独立合成、多实例天然隔离;手机与平板沉浸形态走 Index 根 Stack 内 DialogHost 浮层实例,零窗口创建开销。所有弹窗带 DialogTitleBar.ets 自绘标题条——系统装饰条在子窗口上不渲染标题字段(AirEDA 同款实证),自绘是全形态视觉一致的唯一方案。导出格式选择也由此从模态 openCustomDialog 迁移为独立子窗实例 ExportFormatPage,可与其他弹窗并发。

▍五、适配中遇到的几个难点

### 难点一:patchelf 破坏 ELF 段布局,dlopen 即崩

现象是应用启动即 SIGSEGV。根因是二进制 patchelf 0.18 在改写 NEEDED/RPATH 时会把 .dynsym/.dynamic 搬进尾端对齐的 LOAD 段,鸿蒙 musl 的 reloc_all 无法接受这种布局——P2 阶段曾实测 116 个库被同一工具污染。解法是彻底弃用二进制 patchelf:所有 ELF 编辑改走 entry/src/main/cpp/elf_edit.py 原位编辑(保持段布局,新名长度不超过旧名),并用 native/scripts/unpatchelf.py 修复历史污染。这条教训被写成构建手册的硬性禁令。

### 难点二:日志系统反过来杀死应用

两轮"假死"最终都指向同一个平台差异:hilog 有进程/域流控,流控停读后 OH_LOG 在 datagram socket 上的发送会永久阻塞——NAPI dlopen 循环的突发日志曾把发送线程整段挂死(黑屏假活、无 trace 日志);另一轮是 GLib 的 g_message 写盘被安全扫描盯上 *.log 后缀挂起,g_messages_lock 跨线程死锁形成 ANR 循环。工程对策是日志全面改道:GLib/g_print 一律文件优先(gimp_trace.txt,.txt 后缀避开扫描),NAPI 日志同样落盘,仅 ERROR 级别上 hilog,AIRGIMP-INIT 启动里程碑埋点用于定位卡死位置。

### 难点三:事件注入绕过记账路径导致指针闪退

"一碰就闪退(SIGSEGV si_addr=0x8)"的根因在注入实现:got_event 被传入 NULL 事件节点,其内部 unlink 路径 _gdk_event_queue_remove_link(display, NULL) 解引用 node->next(GList 偏移 0x8)。正确做法与 Wayland/broadway 后端同款:先 _gdk_event_queue_append 取得真实节点再传 got_event。同类问题还包括触摸 RELEASE 时序——按钮位先被清掉导致点击无效,修正为事件构建完成后再清记账。P41 又在 dockbook 场景修复了陈旧 implicit device grab 改道(GRABFIX 自愈),说明 grab 状态与事件队列的一致性是这类移植的长期战线。

### 难点四:启动性能与包体积的持续治理

GIMP 全量恢复流程在真机上首帧偏慢,ai_log 的 P33/P34/P38/P39/P40 一系列阶段做了系统治理:P34 拆解出 data_factories_load(画笔/图案/渐变/字体工厂)410ms 位于 gimp_restore 内属于用户可见启动时长;P40 移植 AirPhoto 的 deferred-data-load 补丁,no_data/no_fonts 快速出首帧、1.2 秒后补载(实测 547ms 完成),二次冷启动 ui-ready 从 1523–1569ms 降到 1254ms(约降 20%)。体积侧,compressNativeLibs 开启后 HAP 从约 97MB(Stored 未压缩)降到 56.8MB(Defl 60%)。GIMP 源码树为此按工作区分叉纪律从共享树 APFS 克隆出 native/sources/gimp-master-ps(表观 291MB、零实际磁盘占用),补丁落在分叉上而不动共享副本。

▍六、编译、安装与启动

依赖库通常无需重编:预编译产物已入库于 native/install/ 与 entry/.../libs/。改动 babl/gegl/gtk3/gimp 时按 native/BUILD.md 的全链顺序执行:

bash
source native/scripts/env.sh # PREFIX=native/install,工具链路径
native/scripts/build_babl.sh # 改 babl 时
native/scripts/build_gegl.sh # 改 gegl 时
native/scripts/build_gtk3.sh # 改 GDK ohos 后端(patches/gtk3-ohos)后必跑
native/scripts/build_libintl.sh # 真 gettext(libintl_real.c)
native/scripts/deploy_modules.sh # 重编 gegl/babl 后刷新 entry/libs 运行时模块
./hvigorw assembleApp --no-daemon # cmake configure 自动跑 patch_libs.py


纯应用打包可直接使用标准 hvigorw 流程:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


产物位于 entry/build/default/outputs/default/entry-default-signed.hap。签名配置(含密钥的 build-profile.json5)按仓库安全规范本地维护、不入库。连接设备后安装并启动:

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


构建手册特别注明:hvigor 增量构建不重跑 cmake configure,替换 libgdk/libgtk 后需删除对应 .so 并 touch CMakeLists.txt 强制刷新;重编 GEGL/babl 后必须执行 deploy_modules.sh,否则 sanity 检查因算子缺失卡死。

▍七、当前功能边界

当前版本已覆盖的能力:

- GIMP 3.3.1 引擎在鸿蒙真机上的完整启动:编译打通、直达主界面、全窗口渲染(P1–P16 验证记录);
- 触摸与键盘事件链、工具箱点击命中、菜单激活(P5 起修复并回归);
- ArkUI 原生菜单栏桥接真实 GimpAction,含 P41 的二级"添加照片"菜单(PhotoViewPicker 多选导入);
- 中文化:真 gettext + zh_CN .mo 随包,覆盖 gimp30/gtk30 等域;
- 文件加载与导出、导出格式选择独立子窗(P41 多实例化);
- 弹窗双模式实例化:自绘标题条、可拖拽、可并发、主画布不掉帧;
- 启动性能治理(deferred 数据加载)与压缩打包(HAP 约 56.8MB);
- 图片分享接收(sendData general.image UTD)与"打开方式"入口(viewData)。

尚未覆盖或存在已知缺口的能力:插件进程体系停用后,依赖独立插件进程的能力不在当前产物;GIMP 脚本控制台类交互未见接入记录;P41 登记的 dockbook 标签行拖动滚动属 GTK3 iconview 上游语义缺口,尚未解决;打印与桌面外设链路不在当前阶段范围。因此当前版本更准确的定位是"GIMP 3.3.1 引擎的鸿蒙原生工作区版":图像处理核心与 GEGL 管线来自上游完整能力,平台层(显示后端、事件、弹窗、菜单、国际化)为鸿蒙侧自研替换,且每一层都有权威补丁与脚本支撑重建。

▍八、总结

AirPS 展示了"重型 Linux 桌面应用"移植的一条可复现路径:不改写引擎,而是把引擎依赖的平台服务逐层补齐——GDK 后端替换显示层,标准事件注入入口保住 GTK 的内部记账,内置化改造替代 fork/exec 插件模型,真 gettext 补回国际化。P1 到 P41 的阶段记录同时说明,这类移植的稳定性工作集中在平台接缝处:ELF 布局、日志系统、设备 grab、窗口几何,每一处都以真机复现与像素/日志双证据收敛。

后续工作重心在 ai_log 与构建手册中已有明确线索:dockbook 上游语义缺口的跟进、弹窗拖拽与几何回写的进一步打磨、启动路径在更多机型上的持续验证。仓库以 GPL-3.0-or-later 公开全部移植改动与预编译产物的对应源码,使整条链路保持可克隆、可构建、可审计的状态。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。