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

AirScape(Inkscape)· 鸿蒙 PC 适配全记录

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

■ 应用:AirScape(AirScape)
■ 定位:Inkscape 移植,矢量图形编辑
■ 上游:Inkscape 1.5.0-dev | 路线:GTK4+GDK OHOS | 版本:1.8.0
■ 开源: https://gitcode.com/Andy88/Harmony-Inkscape-OHOS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917611576866785563
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

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

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配 AirScape

Inkscape 是开源矢量图形编辑器的事实标准,钢笔与贝塞尔曲线、节点编辑、路径运算、渐变与网格渐变、文字与对齐分布工具链完整,SVG 是它的母语格式。设计、插图、图标制作这类工作对桌面形态依赖很深,把 Inkscape 的完整引擎带到鸿蒙 PC 与平板上,检验的是鸿蒙生态承载大型 C++ 桌面应用的真实能力——Inkscape 主体约 70 万行 C++ 代码,依赖 GTK4 在内的数十个库,任何一层都是工程量的量级考验。

AirScape 的选择是全量交叉编译而不是重写或网页套壳:Inkscape 与 GTK4 的完整 C++ 原生栈编译为鸿蒙 arm64 共享库(54 个 .so),在 ArkUI 宿主窗口内由 TEXTURE 型 XComponent 承载真正的 Inkscape 窗口。ArkUI 只负责宿主壳:顶部原生菜单栏、文件选择器、启动闪屏、沉浸式窗口与剪贴板,Inkscape 原生的菜单栏与文件对话框在 native 侧被摘除或改道。

当前工程包名为 com.airscape.app,app.json5 中版本为 1.7.0(versionCode 1700000,targetAPIVersion 26),module.json5 声明 deviceTypes 为 tablet、2in1、phone,锁横屏,请求 READ_PASTEBOARD 权限并注册图片类型的收发分享意图。上游为 Inkscape 1.5.0-dev 与 GTK 4.14.5,目标 ABI arm64-v8a,SDK HarmonyOS 6.1.0(23),整体以 GPL-3.0-or-later 开源。

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

Inkscape 桌面版自带一整套 GTK 桌面假设:menubar 由 GMenuModel 驱动,文件对话框是 GTK FileChooser,输入来自 X11/Wayland,窗口装饰与生命周期由桌面环境管理。把这些原样搬上鸿蒙既不可行也不合适——触摸交互、系统 picker、鸿蒙窗口体系都有自己的规范。AirScape 的边界因此是"引擎原样、宿主重造":绘制、文档模型、工具链全部来自原生 Inkscape;菜单、文件选择、窗口管理改由 ArkUI 与系统服务接管。

层次 原项目实现(Inkscape 桌面版) 鸿蒙侧当前方案
应用入口 GtkApplication main() Stage 模型 EntryAbility
窗口承载 GDK X11/Wayland 后端 XComponent(TEXTURE 纹理表面)+ 自研 OHOS GDK 后端
菜单栏 GTK menubar(GMenuModel) native 侧摘除,GMenuModel 导出 JSON,ArkUI Menu/MenuItem 渲染
文件对话框 GTK FileChooser DocumentViewPicker + 沙箱暂存中转(picker URI ↔ POSIX 路径)
渲染路径 cairo / GL(GSK 渲染器) cairo 主干(GL 全链保留在 libgtk,一处切换)
输入 桌面键鼠 ArkUI 采集触摸/键鼠/多指手势合并后注入 GTK 事件循环
剪贴板 GDK 剪贴板 ArkTS 侧经 UDMF 同步系统剪贴板

这条边界也定义了验收口径:画布上每一笔矢量绘制都必须出自 Inkscape 引擎本身,ArkUI 侧不模拟任何编辑能力。

▍三、鸿蒙版的整体架构

启动时序为:ArkTS 先把 rawfile 中的 Inkscape 数据文件(约 30MB,icons/ui/locale 等)并行拷入应用沙箱,完成后经 NAPI 通知 native 解除阻塞;native 线程执行 gtk_init、创建 InkscapeWindow 并渲染到 XComponent 表面;此后触摸与键鼠事件由 ArkUI 持续注入 GTK 事件循环。

text
ArkTS 壳(entry/src/main/ets)
├── 顶部菜单栏 / 文件 picker / 启动闪屏 / 沉浸式窗口 / 剪贴板
└── pages/Index.ets ── XComponent(TEXTURE)纹理表面
│ NAPI:libgtkbridge.so(按序 dlopen 整个 C++ 栈,事件转发)
▼
libairscape_core.so(airscape_core.cpp / inkscape_app.cpp)
├── GTK 线程宿主:gtk_init / 创建 InkscapeWindow / 事件注入
└── picker 桥 / 菜单桥(GMenuModel → JSON)
▼
libgtk-4.so(GTK4 + OHOS GDK 后端,渲染合成/事件)
libinkscape_base.so(Inkscape 主体,InkscapeApplication)
└── + 48 个依赖 .so(glib / gtkmm / cairo / gsl / xml2 等)


仓库主要目录如下:

text
AirScape/
├── AppScope/ # 应用级配置(包名、版本)
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/ # Index(XComponent 宿主)/DialogPage/FormatSelectPage/KeysPresetPage
│ │ ├── entryability/ # 启动、数据拷贝、窗口与 picker 初始化
│ │ ├── managers/ # ImmersiveManager(沉浸式窗口状态分流)
│ │ ├── components/ utils/ # 壳层组件;FileManager/ImportManager 沙箱适配
│ │ └── cpp 层同级的 native 桥接见 cpp/
│ ├── cpp/
│ │ ├── napi_init.cpp # NAPI 薄壳:dlopen 栈 + 事件转发
│ │ ├── airscape_core.cpp/.h # GTK 线程 + InkscapeWindow + core API
│ │ ├── inkscape_app.cpp # InkscapeApplication 启动
│ │ ├── ohos-gdk-patch/ohos/ # GTK OHOS GDK 后端(24 文件)
│ │ ├── patch_libs.py 等 # .so 打包前 ELF 处理流水线
│ │ └── libs/arm64-v8a/ # 54 个打包 .so
│ └── resources/rawfile/inkscape-share/ # Inkscape 数据文件(icons/ui/locale)
├── native/
│ ├── BUILD.md # 构建链全景文档(含踩坑记录)
│ ├── scripts/ cross/ # 交叉编译/部署/签名脚本;meson cross 与 cmake 工具链
│ ├── patches/ # GTK/Inkscape 本地适配 patch(含对照表)
│ ├── theme/ # Adwaita 主题 CSS
│ ├── sources/ build/ install/ install-gtk/ signing/ # 源码树与构建产物
├── ai_log/ # 历次改动详记(根因分析/编译结果/部署状态)
├── README.md ROADMAP.md THIRD_PARTY.md LICENSE


▍四、核心适配过程

### 1. GTK4 的 OHOS GDK 后端

GTK4 通过 GDK 后端抽象平台差异。entry/src/main/cpp/ohos-gdk-patch/ohos/ 下的 24 个文件实现了完整后端:surface 生命周期对接 OHNativeWindow,帧提交走 cairo 的 begin_frame/end_frame,事件从 ArkUI 注入。GTK 重编一律以 patch 形式落在 native/patches/gtk/,构建脚本从共享 GTK 源码树 APFS 克隆后自动叠加,绝不改写共享树。对话框类 surface 单独分类,走 cairo 子窗通道,避免与主窗争夺原生窗口队列。

### 2. Inkscape 主体的 C++20 交叉编译

libinkscape_base.so 是全项目最大的单件:Inkscape 约 70 万行 C++20 代码交叉编译后的完整引擎库。OHOS NDK 的 libc++(clang 15)缺 std::format、std::jthread、std::atomic_ref 等特性,工程提供 compat shim 并逐文件修复;2geom、libcroco、libuemf 等子模块随主体一起编译,C++20 不兼容的 libdepixelize 作为可选功能移除。依赖栈方面,gtkmm/glibmm/cairomm/pangomm/sigc++ 全套 C++ 绑定逐个交叉编译,GSL 以手工方式编译 324 个目标文件后链接,ICU 与 potrace 按实际引用面做最小 stub。

### 3. 菜单桥与文件 picker 桥

菜单走的不是截图模拟:native 侧把 GTK 的 GMenuModel 导出为 JSON,ArkUI 以 Menu/MenuItem 渲染,事件驱动加载替代轮询;「导入图片」等壳层菜单项由 ArkTS 在指定锚点后注入(GMenu label 带助记符后缀,匹配必须用前缀方式)。文件侧对接 DocumentViewPicker:picker URI 与 native POSIX 路径之间经沙箱暂存中转,导出时按格式弹窗选择。「从本机图库」走 PhotoViewPicker 免权限多选,「从附近设备」经分布式文件系统 connectDfs 列出可信设备目录后合并导入。

### 4. ELF 处理流水线

交叉编译产物不能直接进 HAP:patch_libs.py 以双目录参数生成 soname 映射,剥离全部 NEEDED 版本号(如 libinkscape_base.so.1.5.0.0 改写为 libinkscape_base.so)并修复对齐,musl 严格匹配否则 dlopen 失败;hvigor 不打包深传递依赖,gtkmm 栈的依赖在 CMakeLists.txt 的 target_link_libraries 显式列出;nativeLib.debugSymbol.strip 设为 false,所有 .so 在构建期预 strip。这套流水线固化在部署脚本中,是后续一切迭代的前置。

### 5. 体积治理与幽灵 LOAD 段

staging 全量 385MB 的直接原因是 OHOS NDK 链接的大型 C++ .so 尾部存在幽灵 LOAD 段——libinkscape_base.so 114MB 中 77MB 是 align 0x10000 的填充段,llvm-strip 按节表工作删不掉它。strip_phantom_load.py 做段内压实:删除填充、平移 .dynstr、改写 DT_STRTAB、缩段截断且幂等。处理后 staging 降至 108MB,配合 compressNativeLibs:true,HAP 从 243MB 降到 56MB,真机 dlopen 与主题验证正常。

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

### 难点一:接入 Inkscape 后的整屏黑屏(两根因叠加)

libinkscape_base 接入后应用存活但不渲染。定位出两层根因:其一,libgtkbridge.so 的 NEEDED 带版本号(libinkscape_base.so.1.5.0.0),而 hvigor 部署的文件名无版本号,musl 严格匹配导致整个桥加载失败;其二,hvigor 只打包直接引用加一层传递依赖,libinkscape_base 深层的 gtkmm 栈全部缺席。修复分别是 CMake POST_BUILD patchelf 重写 NEEDED,以及在链接清单显式列出全部 8 个传递依赖库。

### 难点二:hvigor 二次 strip 破坏 64K 对齐

预 strip 过的 libgtk-4.so 被 hvigor 的 llvm-strip 再次处理,64K 对齐的 PT_LOAD 被重排为非法 ELF,设备 dlopen 直接 SIGSEGV。对策是 build-profile.json5 关闭 debugSymbol strip、构建期统一预 strip 加对齐修复。这条教训后来又演化出"strip 静默失效"的排查纪律:体积问题以 readelf 的 LOAD 布局判定,不以是否执行过 strip 判定。

### 难点三:GL 渲染器的十层根因与 TEXTURE 表面

GL 链路激活战役在真机上逐层定案了十个根因:libepoxy 试图 dlopen Linux soname、toplevel 早 realize 时原生窗口为 0、surfaceless make_current 语义错误、gl_version 不回填、is_current vfunc 缺失、缓冲几何锁错、对话框表面必然 BAD_ALLOC、上下文不共享纹理隔离,最深一层是 TEXTURE 型 XComponent 不呈现 EGL 缓冲——eglSwapBuffers 成功而屏幕字节级不变。最终架构改为 GL 全离屏:上下文挂 pbuffer,end_frame 中 glReadPixels 后走与 cairo 相同的 CPU 生产者通道上屏。实测 GL 加 CPU 桥的 flush 成本高于 cairo 主干,当前默认渲染器保持 cairo,GL 全链保留待 NEON 通道交换优化后评估。

### 难点四:桌面交互语义与触摸设备的冲突

tooltip 滞留的根因是 GTK 的 tooltips_enabled 双闸:触摸按压事件带 TOUCHSCREEN 源与 emulating 序列,鼠标 hover 弹出的气泡在触摸交互后收不到 hide。修复以 patch 落在 GTK 层(TOUCH_BEGIN 直通门控并归入 hide 分支)。暗黑模式适配则踩中 onConfigurationUpdate 回调里无条件调用 setColorMode 的无限递归——回调自身触发 configuration 变更,进程两秒内栈溢出崩溃,修复为仅在 colorMode 与目标不一致时重设。

### 难点五:云相册大图导入的转码链

PhotoViewPicker 选中云相册 HDR 大图(11MB jpg)后,packToData(PNG) 报 62980119 Packing failed,jpg 直接透传又因 gdk-pixbuf 仅有 PNG loader 而落成空白文档。FileManager.ensureOpenableImage 建立两级兜底:先强制 RGBA_8888 解码剥离 HDR/YUV,失败再解码重打包并对超 4096 长边等比缩放,真机复测转码成功上画布。

▍六、编译、安装与启动

交叉编译链在首次构建或依赖变更时执行,顺序如下(详见 native/BUILD.md):

bash
source native/scripts/env.sh # 交叉环境
bash native/scripts/build_deps_d2.sh # 依赖库 → native/install/lib
bash native/scripts/build_gtk_with_gl.sh # GTK4 → native/install-gtk/lib
cp -f native/install-gtk/lib/libgtk-4.so.1.* \
entry/src/main/cpp/libs/arm64-v8a/libgtk-4.so # staging
bash native/scripts/build_inkscape.sh configure
make -C native/build/inkscape inkscape_base -jN # libinkscape_base.so
python3 entry/src/main/cpp/patch_libs.py \
entry/src/main/cpp/libs/arm64-v8a native/install/lib # NEEDED 去版本号 + 对齐


日常只改 ArkTS 或桥接时,直接进行 HAP 构建:

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,安装启动:

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


排障入口是 hilog 中带 AirScapeNAPI 标记的 dlopen 记录:启动白屏只剩菜单即 dlopen 链断裂,多为 NEEDED 未 patch,重跑双目录 patch_libs.py。

▍七、当前功能边界

当前版本已覆盖的能力:

- 完整 Inkscape 原生工具链:钢笔/贝塞尔/形状/文字/路径运算/渐变/网格渐变/对齐分布,54 个 .so 全栈随 HAP 分发;
- ArkUI 原生菜单栏(GMenuModel 到 JSON 导出,事件驱动),文件打开/保存/导入/导出走系统 picker 与沙箱中转;
- 导入矩阵:SVG/SVGZ、PDF/AI(poppler,含多页与裁剪框)、CDR/CCX(libcdr)、VSD/VDX/VSDX(libvisio)、WPG、DXF(自研原生转换器,覆盖 LINE 至 DIMENSION 与图层单位)、EMF/WMF 与常见位图(非 PNG 经系统 image kit 硬解转码);
- 导出矩阵:SVG/SVGZ、PDF、PS、EPS、PNG、EMF/WMF、ODG、POV-Ray、LaTeX/PSTricks;
- 暗黑模式(Inkscape 原生 preferDarkTheme + GTK dark 变体)、沉浸式全屏按设备形态分流、双指平移捏合缩放与触控板平滑滚动、UDMF 剪贴板互通、自动保存落盘 Download、中文本地化;
- 手机形态锁横屏适配与对话框跟随旋转,Mate 60 Pro/MatePad/Pura X 多机回归记载于 ROADMAP。

尚未覆盖或仍在推进的能力:

- XCF 与 EPS 导入:上游依赖 Python 扩展与 ghostscript,鸿蒙无 Python 运行时,属当前架构下物理不可达项;
- GL 渲染器:全链已修复并保留,但纹理上传黑块与 CPU 桥逐像素转换未优化,默认仍为 cairo;
- tooltip 修复的鼠标场景回归、跨设备分布式导入的端到端文件传输、Pura X 空闲 CPU 偏高(约 15%)的隔离排查,均记载为待验证项。

当前版本更准确的定位是"原生 Inkscape 引擎的鸿蒙三形态移植版":编辑能力与格式矩阵以 Inkscape 上游 C++ 原生能力为界,不做任何引擎外模拟;宿主体验层(菜单、picker、窗口管理)按鸿蒙交互规范重造。

▍八、总结

AirScape 的工程价值在于把"大型 GTK 桌面应用整体移植"这条最难的路走通了一遍:GTK4 后端、gtkmm 绑定栈、Inkscape 主体、ELF 处理流水线与体积治理全部沉淀为可复用资产,构建链文档化到脚本级。它证明鸿蒙的 NDK、XComponent 与窗口体系足以承载 70 万行量级的 C++ 桌面引擎,而不是只有重写一条路。

后续工作的重心在 ROADMAP 中已有明确排期:cairo 主干的合成快路径与光栅边距继续收敛帧方差,GL 通道完成 NEON 化后重新评估切换,分布式导入与鼠标回归逐项销号。每一项都沿用同一验收方式——hilog 探针定量留证加真机视觉确认,避免"编译通过"替代"行为正确"。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。