Air 系列应用交流/AirReader(from Okular)|AirReader · 鸿蒙 PC 适配全记录

AirReader(from Okular)|AirReader · 鸿蒙 PC 适配全记录

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

■ 应用:AirReader(from Okular)(AirReader)
■ 定位:Okular 移植,文档阅读器(PDF 等)
■ 上游:Okular 26.04.3 | 路线:Qt6 6.11.1+KF6 | 版本:1.1.1
■ 开源: https://gitcode.com/Andy88/Harmony-Okular
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615885201602898
■ 官网(免费下载): https://www.shunidc.cn/

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

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

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

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

▍一、为什么要适配 AirReader

AirReader 的移植对象是 KDE 生态的文档查看器 Okular 26.04.3。Okular 长期是 Linux 桌面上功能最完整的文档阅读器之一,依托 libOkular6Core 与 generator 插件体系支持 PDF、DjVu、EPUB、Mobipocket、纯文本等多种格式,并带有目录树、缩略图侧栏、注释工具箱与书签体系。在 PC 上"看文档"是比编辑更高频的动作,把这样一款多格式文档查看器带到鸿蒙 PC,价值不在演示而在日常可用。

这个工程的移植采用 AirCad 验证过的 Qt6 QPA 范式:libqohos QPA 插件承载原生窗口,薄 NAPI 桥负责注入,重型 core 跑在 Qt 宿主线程,全栈动态库随 HAP 分发。Okular 依赖 Qt 6.11.1 的 15 个模块和 KF6 6.20.0 的 28 个包,再加上 poppler 26.08、djvulibre 3.5.28、libepub、QMobipocket6 等格式库,是这一范式迄今负载最重的一次实践。

当前工程包名 com.airreader.app,版本 1.0.0,deviceTypes 覆盖 phone、tablet 与 2in1,目标 ABI arm64-v8a,要求 HarmonyOS 6.1(API 23)以上。签名 HAP 约 228 MB,内含 92 个预编译 .so 与 okularbridge、okular_core 两个自研壳库。

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

Okular 原版是一个 KParts 应用:shell 进程通过 KXmlGui 建立菜单与工具栏,part 承载文档视口,文件读写走 KIO。移植到鸿蒙后,shell 不再存在,KIO 没有 worker 进程,gettext 在 musl 全栈缺失。工程的做法是保留 part 与全部 generator 的原实现,把窗口宿主、菜单、文件选择、保存、翻译这些"桌面系统服务"逐个换成鸿蒙等价物,换不动的明确登记而不是遮掩。

层次 Okular 原版实现(Linux 桌面) 鸿蒙侧当前方案
应用入口 shell 进程 main.cpp + KParts 宿主 Stage 模型 EntryAbility + ArkTS Index.ets 壳
窗口承载 QWidget + X11/Wayland XComponent(TEXTURE) + libqohos QPA(EGL 上屏)
菜单/工具栏 KXMLGUI(okularui.rc) 菜单树导出 JSON,ArkTS 顶栏系统 bindMenu 渲染回注
文件打开 KIO worker + KFileWidget DocumentViewPicker + 沙箱 staging 中转
保存/导出 KIO::file_copy airLocalFileCopy 手工流式拷贝(PATCH-AIRREADER-10)
国际化 gettext + libintl 自写 mini-gettext 替换 libintl.so(完整 MO 解析)
图标资源 系统图标主题 breeze SVG 随 share.tar 部署 + 显式 themeSearchPaths
剪贴板 QClipboard 直连系统 dataChanged → NAPI → pasteboard 桥

这份对照就是工程的验收口径:每一行右侧的实现都对应真机上跑通的一条链路,而不是"能链接"的静态声明。

▍三、鸿蒙版的整体架构

PORTING.md 记载的运行时架构如下:ArkTS 壳只保留 XComponent、选择器/IME 桥与资源部署门控;NAPI 桥按拓扑序 dlopen 全部依赖库后把控制权交给 core;core 在 Qt 宿主线程里组装 KParts 主窗口,文档能力由 okularpart 经插件清单加载 12 个 generator。

text
ArkTS 壳 Index.ets(XComponent TEXTURE + splash + picker/IME 桥 + rawfile 部署幂等门控)
└ libokularbridge.so(薄 NAPI:61 库拓扑序 dlopen RTLD_GLOBAL → dlsym core/QPA 注入)
├ libqohos.so(Qt6 QPA:EGL 上屏 + QOhos_Inject* 输入 + 系统选择器 helper)
├ libokular_core.so(Qt 宿主线程:静态注册 QPA → 预绑定 NativeWindow → QApplication
│ → AirReaderMainWindow(KParts::MainWindow) → QPluginLoader(okularpart.so)
│ → createGUI(part);openUrl 走本地文件快路径,无 KIO worker)
└ okularpart.so → libOkular6Core.so → 12 generator(清单 dlopen)


主要目录如下:

text
AirReader/
├── AppScope/app.json5 # 包名 com.airreader.app、版本 1.0.0
├── PORTING.md # 移植全记录:管线、补丁清单、逐轮真机验证
├── entry/src/main/
│ ├── ets/pages/Index.ets # ArkTS 壳:XComponent、选择器/IME/剪贴板桥、标签条
│ ├── ets/utils/ImmersiveManager.ets # 沉浸全屏管理(平板全屏 / 2in1 自由窗分形态)
│ ├── ets/entryability/ # EntryAbility 入口
│ ├── resources/rawfile/okular/ # generators.conf + share.tar(48MB 资源包)
│ └── cpp/
│ ├── napi_init.cpp # NAPI 桥:dlopen 拓扑序、输入与回调透传
│ └── okular_core.cpp # Qt 宿主线程、KParts 主窗口、菜单导出/回注
└── native/
├── scripts/ # env.sh、build_kf6.sh、build_okular.sh、deploy.sh 等
├── patches/ # karchive/okular/djvulibre/ebook-tools 上游补丁
├── sources/ # 上游源码树(Qt、KF6 28 包、poppler、okular 等)
├── qt_platform/ # libqohos QPA 构建
└── cross/ host-install/ install/ # 交叉前缀、宿主工具与安装栈


▍四、核心适配过程

### 1. Qt6/KF6 全栈交叉编译管线

native/scripts/ 是整个移植的地基。env.sh 建立 NDK SDK24 交叉环境并借用 AirGis(Qt)、AirGimp(jpeg/tiff/freetype/lcms 等)、AirCad(zlib/png/icu)三个安装栈,同时设 CMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY 堵住 host 包泄漏。build_host_tools.sh 用 homebrew Qt 编出 kconfig_compiler 等 host 版构建期工具经 KF6_HOST_TOOLING 注入;build_kf6.sh 按拓扑序编 28 个 KF6 包并关闭 QML/X11/Wayland/Python 绑定;build_formats.sh 处理 dbus、poppler(无 cairo/fontconfig,DCTDECODER=libjpeg)、djvulibre、libepub 与 QMobipocket6;build_okular.sh 以 OKULAR_UI=desktop 配置 Okular 并强制不依赖 Wallet/Phonon/Spectre 等可选件。deploy.sh 收集产物时做去版本号、NEEDED 路径修复、ELF 对齐三件套,再硬链进 entry/libs。

### 2. 无 shell 的 KParts 宿主

Okular 的 shell 子目录被 PATCH-AIRREADER-3 从构建中剔除,KParts::MainWindow 宿主由 cpp/okular_core.cpp 自行提供:静态注册 QPA、预绑定 NativeWindow、创建 QApplication,再以 QPluginLoader 加载 okularpart 并 createGUI。generator 的发现改为 PATCH-AIRREADER-2 的 OKULAR_GENERATOR_MANIFEST 清单机制——沙箱里无法遍历插件目录,就由随包的 generators.conf 逐个 dlopen。宿主还需补上 shell 原本负责的两件事:setXMLFile("shell.rc") 让 KXMLGUI 菜单得以建立,setApplicationDomain("okular") 让 i18n 不落空域。

### 3. 文件选择器与保存链路

打开走 ArkTS DocumentViewPicker,选中文件经沙箱 staging 中转后交给 part 的 openUrl 本地快路径。保存链路比打开复杂得多:QPA 文件对话框 helper 的 reqId 带高位标志,早期被菜单通道误吞导致区域保存整体冻结,修复为按 0x40000000 位分流;保存模式下 ArkTS 调 DocumentViewPicker.save() 并以不预建的 filesDir/okular/out/ 中转路径应答 Qt(预建会触发不可见的 QFileDialog 覆盖确认模态),Qt 侧 QSaveFile 写完后由 ArkTS watcher 轮询稳定再拷出到用户所选 URI。part 内部的 KIO::file_copy 由 PATCH-AIRREADER-10 的 airLocalFileCopy 接管,后又在 PC 实测中发现 QFile::copy 的 sendfile 优化路径在 OHOS 虚拟挂载上返回 EOVERFLOW,最终改为 64K 手工流式拷贝,另存为在 MateBook Pro 端到端落盘验证通过。

### 4. 汉化与图标资源

OHOS 全栈无 gettext,KF6 的 .mo 翻译结构性失效。工程自写 mini-gettext 替换 libintl.so:完整解析 MO 文件、导出 g_libintl_* 全套 ABI、未命中返回原指针。配合 41 个 zh_CN catalog 与 Qt 自家 qm 翻译,菜单、对话框与启动页全部中文。breeze 图标共 19.8k 个 SVG,经 share.tar 单包随 rawfile 携带,native 侧 ustar 解包展开,QIcon::setThemeName("breeze") 加显式 themeSearchPaths 双保险后工具栏图标正常渲染。

### 5. 菜单栏的 ArkTS 化

Qt 的 KXMLGUI 菜单树经 menu_json_append 导出 JSON,ArkTS 顶栏用系统 Menu/MenuItemGroup/MenuItem 渲染,激活经 activateMenuAction 回注 QAction,label 双复核防错配。这条链路经历了 bindMenu builder 失效、bindPopup 吞点击、自研覆盖层与飞出面板三轮绕行后,最终回到系统 bindMenu 递归 builder 的基准形态,自绘代码全部删除。子菜单级联、快捷键与勾选态均由系统组件自带。

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

### 难点一:单 Surface 下模态对话框不可见

part 的另存为、配置等动作会弹 Qt 原生模态对话框,单 Surface QPA 下既不可见又阻塞主线程,表现为整体冻结。工程先在 buildGui 时灰化高危动作,再逐步实装保存桥,最后用 QApplication 子类重载 notify() 拦截 QDialog 型顶层窗口,把标题、正文与按钮列表上抛 ArkTS 的 promptAction.showDialog,选择结果回注 Qt click。实证案例是损坏 PDF 打开失败的 KMessageBox 以鸿蒙圆角卡片呈现且回注后零卡死。

### 难点二:rawfile 大规模资源的随机故障

两万级文件直放 rawfile 会在真机随机挂起,getRawFileContent 通道又会随机产出 0 字节文件——generators.conf 变 0 字节直接导致"找不到 plugin"。解法是把 share 数据打成单 tar(48MB),rawfile 只留 2 个条目,ArkTS 以 RawFileDescriptor 分块拷贝,native untar_ustar 确定性展开;普通文件一律走分块通道并加 stat 校验、版本戳与"清单空则强制重部署"自愈。

### 难点三:设备换代暴露的六条独立根因

从 MatePad Mini 迁到 MatePad 11.5 后出现 PDF 打不开、无菜单栏、图标纯文字、全英文、core 起不来五连回归,逐一排查出六条独立根因:11.5 系统命名空间不再提供 libfreetype 导致 poppler 断供(自编 2.13.3 真库入包);XDG 环境变量设置晚于 dlopen 触发的 Qt 静态初始化,路径被永久缓存(setenv 全部前移);KPluginMetaData 构造喂错了 JSON 层级导致 generator 永不匹配 mimetype;Qt6Qml 依赖从借用栈泄漏(CMAKE_DISABLE_FIND_PACKAGE_Qt6Qml 根治)等。旧设备的残留数据曾掩盖其中大半,这轮之后工程确立了"换机必做全链回归"的纪律。

### 难点四:surface 回调与初始化的竞态

OnSurfaceCreated 早于 SetDataPath 到达时,NativeWindow 句柄被丢弃,QPA 永远等不到 EGL surface,界面空白且"随机不渲染"。修复是 NAPI 侧缓存 surface 句柄,SetDataPath 后补注入;排查判据是核心日志缺 NativeWindow injected 行。同类双层 pending 协议后来也用在 xcOffset 探测上。

### 难点五:触摸输入的合成断链

Qt 的触摸到鼠标合成在 11.5 真机上不抵达控件,单指点击全部落空。工程定型为单指直接注入 QOhos_InjectMouse(tap 点击、拖动平移,与阅读器主交互天然匹配),多指走 QOhos_InjectTouch 支持捏合缩放,坐标使用 XComponent 触摸点的本地物理坐标而非 screenX/screenY,并补 CANCEL 的 release 防止按键卡死。

▍六、编译、安装与启动

Native 全栈先行,再走标准 hvigor 打包。native 侧按依赖顺序执行脚本:

bash
cd native/scripts
bash env.sh # NDK SDK24 交叉环境 + AirGis/AirGimp/AirCad 三借用栈
bash build_host_tools.sh # host 版 KF6 构建期工具
bash build_kf6.sh # KF6 28 包拓扑序
bash build_formats.sh # dbus/poppler/djvulibre/libepub/QMobipocket6
bash build_okular.sh # Okular 26.04.3(OKULAR_UI=desktop)
bash deploy.sh # .so 收集修复三件套 + rawfile 部署
cd ../..


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 约 228 MB。注意 PC 消费设备拒装 release 证书侧载包,签名需用含设备 UDID 的调试 profile。安装启动:

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


PORTING.md 记载 2026-09-11 在 MateBook Pro(2in1)上的最终验证:PDF/EPUB/TXT 打开渲染、翻页、目录树、菜单一次点击切换、复制到系统剪贴板端到端全部通过,零崩溃。

▍七、当前功能边界

当前版本已覆盖的能力:

- 12 个 generator 的文档打开与渲染(PDF/poppler、DjVu、EPUB、Mobipocket 及内置纯文本等);
- 翻页、缩放、适合整页、两指捏合、拖动滚动;
- 中文菜单栏(文件/编辑/视图/跳转/书签/工具/设置/帮助)与 breeze 图标工具栏;
- 目录树、缩略图侧栏、注释工具箱(tools.xml 高亮工具);
- ArkTS 多标签:每文档独立 okularpart 实例,标签条左对齐、picker 文件去重激活;
- 系统选择器打开 + staging 中转;保存/另存为/导出端到端落盘(reqId 位分流 + watcher 拷出);
- 复制选中文本到系统剪贴板(S9 桥 + PATCH-AIRREADER-9 Browse 分支回退);
- 注释输入框 IME 自动面板(notify() 焦点总闸 + QPA 路径双保险);
- Qt 模态对话框桥接为鸿蒙系统弹窗;平板沉浸全屏、2in1 自由窗保持窗口化。

尚未覆盖或已登记的边界:

- 非内嵌基础字体的 PDF 文字不渲染(无 fontconfig 替代字体查找,真实文档多为内嵌字体);
- 手写笔、per-window 光标样式、碰一碰/系统分享、暗黑模式未实现(PORTING.md §9 审计表 S4/S6/S10/S11 未做);
- 打印未纳入验证记录;打开 .okular 归档后另存等场景仍存在不可见确认弹窗的触发风险;
- 选择器关闭后 XComponent 纹理偶发丢失(个别时序,重启恢复);缩略图首格偶现灰块。

因此,当前版本更准确的定位是"Okular 鸿蒙多格式文档查看基础版":核心阅读链路在手机、平板与 PC 三类形态真机闭环,桌面系统集成项按审计表逐项补齐。

▍八、总结

AirReader 的移植把 Qt6 QPA 范式从单一 GUI 应用推进到了 KParts 插件体系与 28 包 KF6 的规模:92 个动态库随 HAP 分发,generator 以清单机制在沙箱内加载,KIO、gettext、X11 这些桌面基础设施逐一换成鸿蒙等价物。PORTING.md 里 45 条编号修复与逐轮真机记录,本身就是这轮移植最有复用价值的产出。

后续工作的重心在审计表与遗留清单里已经写明:补齐手写笔与分享等系统能力、部署 breeze-dark 支持暗黑模式、收敛弹窗桥以消除归档另存的冻结风险,并把 2in1 触控板场景下的双模输入验证补全。每项都以真机端到端行为为验收,延续这个工程一贯的实证口径。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。