鸿蒙 PC 上的 PDF 与 Markdown 阅读、转换应用。本阶段先把文字型 PDF 转成沙箱内的 MD 文件。
普通克隆主分支后,在项目根目录执行这一整条命令,下载五个独立仓库的固定版本:
git submodule sync --recursive && git submodule update --init --recursive主项目只保存业务代码、第三方桥接/构建适配和子模块指针;PDFium、MarkItDown、MD 核心、
PortOne、DOC 转换核心的源码分别位于 third_party/{pdfium,markitdown,markdown,word,doc2docx}/source/。
未初始化时这些源码目录为空,Run 会提示并停止。源码修改在对应子模块提交和推送,
外层适配修改在主项目提交;详见 第三方维护规则。
Qt/CEF 继续使用原来的固定预编译 SDK/.so,不新增内核源码或改变固定包。
本项目自有代码采用 PolyForm Noncommercial License 1.0.0,官方原文见 PolyForm 网站。这是源码公开的非商业使用许可。
允许在许可规定的用途范围内使用、学习、研究、修改和分发。商业产品开发、销售、商业服务等超出许可范围的用途,需要另行取得相关权利人的授权。许可对教育机构等组织另有允许用途规定,以 LICENSE 原文为准。
第三方源码、依赖和资源仍遵循各自的许可证及版权声明,不受本项目许可证替代。贡献代码前,请确认你有权将该贡献按本项目许可证授权。
- 首页保持 ArkTS;银灰介绍区与主区域按约 30% / 70% 分配宽度,主区域提供文档预览、文档编辑、文档转化三个小正方形入口,普通字重的名称放在方块外下方。点击入口打开独立格式选择子窗口;预览中的 Word(DOC/DOCX)、PDF、MD 入口打开单栏 Qt 工作区,编辑仍只展示格式;转化窗口中的 PDF→MD 入口打开双栏工作区。本次 DOC 预览接入仅做格式和静态检查,尚未重新编译、打包或设备验收。
- 左侧介绍区上下内边距为 50vp、左右为 32vp;标题置顶,两段介绍组成间隔 20vp 的底部内容组,外层通过
SpaceBetween分布并保持左对齐。 - 介绍区背景与黑色 ∞ 使用独立 SVG:银灰底图包含左上、右下的淡化文档及斜向柔光,∞ 的反光从中心向两侧连续衰减;底图等比例铺满,符号等比例居中,文字独立叠加,整体由 24vp 圆角裁切。本次素材与布局仅做静态检查和设计渲染,尚未编译、打包或设备验收。
- 首页右侧入口改为顶部排列,下方“最近工作区”默认展示三条任务目录记录;当前可恢复 PDF→MD 转化任务及 PDF/MD/DOC/DOCX 单栏预览任务,沿用原文件,并在工作区载入后更新任务目录最近使用时间。本次最近列表、恢复与时间更新仅做格式检查,尚未编译、打包或真机验收。
- ArkTS 首页左侧有 60vp 半透明窄侧栏,底部 36vp 三横线 SVG 按钮可打开“关于、外观、截图、文件管理、设置”菜单。菜单通过实际布局坐标,让菜单底部与按钮底部对齐,菜单左边与按钮右边保持 8vp 间距;外观二级菜单提供浅色、深色、跟随系统,选择持久保存并应用于 ArkTS 首页和弹窗;Qt 工作区仍使用自身配色。关于弹窗读取实际应用版本号;截图、文件管理、设置本阶段只保留入口并提示暂未开放。首页内容背景与窗口容器分别设置透明,焦点态容器透明,非焦点态使用实色,右侧主内容保持不透明。
- Qt 开发依赖保存在
third_party/qt/,运行库从entry/libs/arm64-v8a/链接并随 HAP 分发;PDFium fork 保存在third_party/pdfium/source/,Qt 左侧输入区域已接入其公开 C API 实现正文预览;已完成鸿蒙编译、HAP 打包;系统目录跳转沿用原有实现。 - DevEco Run 构建前执行
PrepareNativeDependencies,按当前 PDFium 源码增量生成libpdfium.so;Qt/NAPI 的 C++ 编译后同步新库到libs。没有保存源码自动构建、应用内动态替换库或保持进程状态的热重载。 - Qt 工作区默认最大化并保留系统标题栏和窗口边框,输入、输出区域解耦;中部分割线可拖动,悬浮“转化”按钮随分隔线移动。
- 文档区域共用
DocumentView : QStackedWidget,由工作区注入 PDF、Markdown 或 Word 渲染器;文件卡片、预览切换、忙碌状态与对外信号集中维护。PDF 继承公共RasterDocumentViewport,复用独立绘图窗口、滚动条和惯性能力;CEF 保留浏览器自身滚动。Word 通过 PortOnereadOnly模式显示 DOCX,与 MD 共用CefDocumentPreviewWidget的 CEF 生命周期和原生定位;编辑与保存尚未接入。本次通用化重构仅完成格式检查,尚未编译、打包或设备验收。 - 输入文件卡片通过 DocumentStore 获取系统选取授权,再经 NAPI 调用 C++ harmony_sandbox 完成沙箱复制和路径安排。两侧操作统一到顶部“文件”菜单:查看模式默认预览,可切换为占位图;左侧由 PDFium 显示多页 PDF,右侧已有结果时由 CEF 显示 MD。转换成功自动切到预览并隐藏转化按钮;更换文件保留当前查看方式,清空旧结果并恢复按钮,取消选择保留原状态;尚未生成 MD 时仍显示等待提示。PDFium 后台串行渲染可见页,连续尺寸变化合并请求;位图缓存上限为 64 MiB,超长页面降低采样分辨率。Qt ConversionService 仍复用 PythonRunner 转换。本次默认预览及按钮显隐调整仅做格式检查,尚未编译、打包或真机验收。
- 原有 Python HNP 自检与 PDF 转换已由用户在真机验证成功。新增 Qt 窗口交接、选择与结果位置操作需要真机验收。
- 汇文 Python 包公开
pdf_to_md()与独立的doc_to_docx()。DOC 转换使用纯 Python 核心,打成私有huiwen_doc2docx.hnp,复用原有 Python 解释器;Qt 后台服务调用RunDocConversion(inputPath, outputPath)。每次 Run 按内容准备 HNP 并随 HAP 打包。Word 选择器已放开 DOC,Qt 选取/恢复 DOC 时显示加载提示,后台生成任务内临时 DOCX,再交给原有预览器;更换文件或关闭工作区后清理临时副本。本机包内执行已通过真实 DOC 正文/表格样本测试,用户已确认当前 DOC 预览基本链路可用,详见 DOC 业务流程与文件映射。 - Python 部署拆成稳定环境 HNP 与业务
.pyz:修改汇文 API、MarkItDown 源码或业务补丁只更新 pyz;修改解释器、目标依赖及环境打包配置才重建 HNP。本次拆分仅做语法和格式检查,首次迁移及设备转换尚待验证。
以下以当前仓库根目录为基准。已有表示工作区中存在,生成表示本机打包产物,规划表示后续阶段的目标目录或文件;规划项尚未创建。
HuiWenMCP/
├── AppScope/ [已有] 应用名称、图标和版本
├── entry/ [已有] 鸿蒙 HAP 模块
│ ├── src/main/
│ │ ├── module.json5 [已有] 设备类型、Ability 与私有 HNP 声明
│ │ ├── ets/
│ │ │ ├── abilitystage/QtAbilityStage.ets [已有] Qt 平台初始化
│ │ │ ├── entryability/EntryAbility.ets [已有] ArkTS 启动入口
│ │ │ ├── entryability/QtWorkspaceAbility.ets [已有] Qt 窗口及系统文件操作
│ │ │ ├── components/FunctionSidebar.ets [已有] 首页半透明窄栏与底部菜单按钮
│ │ │ ├── components/HomeFeatureCard.ets [已有] 正方形功能卡片与 SVG 图标
│ │ │ ├── components/HomeFormatDialog.ets [已有] 独立格式选择子窗口与 PDF→MD 跳转事件
│ │ │ ├── components/RecentWorkspaceList.ets [已有] 固定列、文件名中部省略与最近记录点击
│ │ │ ├── pages/HomePage.ets [已有] 首页功能切换与 Qt 窗口交接
│ │ │ ├── pages/Index.ets [已有] QPA 固定加载的 Qt XComponent 承载页
│ │ │ ├── services/ConversionService.ets [已有] 页面调用原生转换的统一入口
│ │ │ ├── services/RecentWorkspaceService.ets [已有] 最近数量、模式文案、时间和恢复前校验
│ │ │ └── utils/DocumentStore.ets [已有] 系统选取授权与 NAPI 适配
│ │ └── cpp/
│ │ ├── CMakeLists.txt [已有] 原生库构建配置
│ │ ├── napi_bridge.cpp [已有] ArkTS → C++ 的 NAPI 接口
│ │ ├── python_runner.cpp/.h [已有] 部署内置 pyz 到缓存,使用 HNP 解释器执行
│ │ └── types/ [已有] 原生模块的 ArkTS 类型声明
│ ├── libs/arm64-v8a/ [生成] Qt、PDFium 与本项目动态库,不进入 Git
│ │ └── libQt5{Core,Gui,Widgets,Concurrent}.so、QPA、libpdfium.so、libhuiwen_{native,qt}.so
│ ├── build-profile.json5 [已有] HAP 构建与 ABI 配置
│ ├── hvigorfile.ts [已有] 按输入准备 Python HNP,并在 HAP 签名前注入
│ └── oh-package.json5 [已有] 模块依赖
├── converter/ [已有] 可供桌面端复用的 Python 转换逻辑
│ ├── huiwen_converter/ [已有] 汇文公开 Python API
│ │ ├── __init__.py [已有] 仅导出已实现的 pdf_to_md()
│ │ ├── api.py [已有] 路径校验、写入 MD、返回输出路径
│ │ └── _markitdown_adapter.py [已有] 唯一直接调用 MarkItDown 的适配文件
│ └── pdf2md/convert.py [已有] pyz 的 __main__,也可供桌面直接执行
├── qt_app/ [已有] Qt Widgets 工作区,鸿蒙共享库/桌面可执行入口
│ ├── CMakeLists.txt 构建 huiwen_qt,链接 Qt、PDFium 与鸿蒙 CEF
│ ├── main.cpp 默认最大化并保留系统边框,显示成功后通知结束原首页
│ ├── resources/
│ │ ├── branding/、file-icons/ 应用图标及共用文件格式图标
│ │ └── file-icons.qrc、markdown.qrc、word.qrc 图标与离线预览页面资源
│ ├── tests/
│ │ ├── CMakeLists.txt、pdf_preview_check.cpp 通用 View、PDF 加载与滚动检查
│ │ └── sandbox_startup/ 沙箱启动检查;检查程序均需单独构建运行
│ └── src/
│ ├── workspace/ConversionWindow.h/.cpp 主窗口、两侧渲染器注入与业务接线
│ ├── documentView/ 不区分输入/输出角色的文档区域
│ │ ├── DocumentView.h/.cpp 共用卡片、预览切换、忙碌状态与信号
│ │ ├── DocumentRenderer.h 渲染契约、滚动归属及可选能力配置
│ │ └── renderers/
│ │ ├── pdf/PdfPreviewWidget.h/.cpp PDFium、页面排版、后台渲染与缓存
│ │ ├── markdown/MarkdownPreviewWidget.h/.cpp MD 渲染器入口
│ │ ├── word/WordPreviewWidget.h DOCX 只读渲染器入口
│ │ └── web/CefDocumentPreviewWidget.h/.cpp MD/Word 共用 CEF 承载
│ ├── components/
│ │ ├── FileCard.h/.cpp 共用文件占位卡片,图标与名称由调用方传入
│ │ ├── ConvertButton.h/.cpp 独立悬浮转化按钮
│ │ ├── rendering/
│ │ │ ├── RasterDocumentViewport.h/.cpp 栅格文档共用视口,组合原生窗口与滚动能力
│ │ │ └── RasterRenderWindow.h/.cpp 独立 QWindow/QBackingStore、脏区与输入转发
│ │ ├── scrolling/DocumentScrollController.h/.cpp Qt 平滑滚动、触控板采样及可选惯性
│ │ ├── splitter/WorkspaceSplitter.h/.cpp 分隔线与拖动位置指示条
│ │ ├── scrollBar/DocumentScrollBar.h/.cpp 文档竖向滚动条样式
│ │ └── functionBar/
│ │ ├── FunctionBar.h/.cpp 顶部“文件”和“滚动设置”入口
│ │ ├── FileMenu.h/.cpp 查看方式、任务目录及当前业务模式菜单
│ │ └── SelectionMenu.h/.cpp 共用菜单样式、右侧蓝点与子菜单箭头
│ ├── services/ConversionService.h/.cpp 异步 PDF 转 MD API
│ ├── sandbox/
│ │ ├── DocumentItem.h 格式、路径、归档目录、缓存打开路径和使用时间
│ │ ├── SandboxStore.h/.cpp 三类目录、最近记录、编辑缓存和路径校验
│ │ └── SandboxService.h/.cpp 请求系统选择、接收沙箱路径和打开输出位置
│ └── platform/harmony/
│ ├── qt_bridge.h/.cpp Qt ↔ ArkTS 线程桥
│ ├── harmony_sandbox.h/.cpp 鸿蒙沙箱规则与文件操作
│ ├── harmony_sandbox_napi.cpp 异步导入与输出校验 NAPI
│ └── CefSubprocess.cpp CEF 子进程入口,不承担文档区域布局
├── hnp/arm64-v8a/ [生成] 鸿蒙 Python 运行环境,不进入 Git
│ └── huiwen_python.hnp 私有 HNP 归档,内部包含:
│ ├── bin/python3 Python 解释器
│ ├── lib/python3.14/site-packages/ PDF 依赖及原生 .so,不含汇文 API 和 MarkItDown
│ └── share/huiwen/python_smoke.py 固定的解释器自检入口
├── entry/src/main/resources/rawfile/python/ [生成] 独立业务包,不进入 Git
│ └── huiwen_converter.pyz
│ ├── __main__.py 来自 converter/pdf2md/convert.py
│ ├── huiwen_converter/ 汇文公开 API 与适配实现
│ ├── markitdown/ 已应用业务补丁的第三方 Python 源码及包内资源
│ └── licenses/markitdown/ MIT 许可与第三方声明
├── packaging/ [部分已有] 各平台打包脚本和配置
│ ├── ohos/ [已有] 应用级 HNP、业务 pyz 与 HAP 打包/检查
│ │ └── build_python_bundle.py 生成确定性业务 pyz,不变则保留成品与时间戳
│ ├── windows/ [规划] Windows 发布配置
│ └── macos/ [规划] macOS 发布配置
├── tests/pdf_samples/ [规划] 文字、双栏、表格等转换样本
├── third_party/
│ ├── markitdown/ [主仓库] 兼容补丁与说明,source/ 为 fork 子模块
│ ├── pdfium/ [主仓库] 鸿蒙构建适配,source/ 为 fork 子模块
│ ├── markdown/ [主仓库] 页面/样式/构建适配,source/ 为 MD 核心子模块
│ ├── word/ [主仓库] 库入口/构建适配,source/ 为 PortOne 子模块
│ ├── cef/ [已有] CEF 适配与说明,SDK 由固定依赖包准备
│ ├── prebuilt/ [已有] Qt/CEF 固定依赖清单与准备说明
│ └── qt/ [本机依赖] Qt 头文件、CMake 配置及宿主工具
├── entry/src/main/resources/rawfile/third_party_licenses/ [生成] Qt/PDFium 许可材料,随包分发
├── CMakeLists.txt [规划] Qt 桌面工程入口
├── build-profile.example.json5 [已有] 不含签名信息的全局构建配置模板
├── build-profile.json5 [本机] 签名配置,已被 Git 忽略
└── README.md [已有] 项目状态与维护说明
当前 Qt 转换链为 ConvertButton → Qt ConversionService.convertPdfToMd() → PythonRunner → HNP Python 执行缓存 pyz/__main__.py → huiwen_converter.pdf_to_md() → MarkItDown → convert/<时间戳_文件名>/output/*.md。Python API 与第三方库适配保持原有边界。原 ArkTS 转换接口仍保留;首页通过“文档转化 → PDF→MD”进入 Qt,不再选文件或直接执行转换。
鸿蒙跳转由 HomePage.ets → startAbility(QtWorkspaceAbility) → QPA.startQtApplication() → libhuiwen_qt.so/main() 完成。Qt 窗口实际显示后通过 NAPI 线程安全回调通知新 Ability;新 Ability 通过应用 EventHub 通知首页,只有发起跳转的首页消费一次通知并调用 terminateSelf()。这只结束首页 Ability,不结束应用进程。
返回链为 ConversionWindow::closeEvent() → QtBridge.request("close") → QtWorkspaceAbility.returnHome():先启动首页,收到 EntryAbility 页面加载且进入前台的 huiwen.home.ready 后,经 workspaceReply({action:"close"}) 通知 Qt 隐藏窗体并关闭浏览器实例;全部实例关闭后 Qt 回传 closed,工作区 Ability 才结束。QPA 已把系统标题栏关闭转为 Qt closeEvent,不重复注册关闭监听。CEF 平台的关闭请求也复用 returnHome()。
QPA 在进程内只启动一次 Qt 主循环,CEF 引擎也保留到应用退出。每次工作区注册/解除现有 NAPI 回调时,Qt 创建/释放对应的 ConversionWindow,下次进入仍使用 showMaximized();关闭工作区不调用 CefShutdown()。窗口管理暂放 main.cpp 与 closeEvent(),后续再迁入窗口基类。本次仅做格式检查,连续往返和 CEF 展示仍需真机验收。
当前 QPA 固定加载 pages/Index,不能让该路由同时作为 ArkTS 首页。EntryAbility 加载独立的 pages/HomePage;Index.ets 仅创建 Qt 的 NODE 类型 XComponent。QtWorkspaceAbility.newLocalStorage() 提供存储,QPA 写入 idName 并传给承载页。若在承载页放首页事件监听,会在 Qt 就绪时把 Qt Ability 也关闭;缺少存储方法则会产生 newLocalStorage not found、Need object 等日志。
文件选择链为 DocumentView::cardActivated → ConversionWindow::selectDocument() → SandboxService → QtBridge → QtWorkspaceAbility → DocumentStore。Qt 请求进入 ArkTS 线程处理;结果经 NAPI 投递到 Qt 线程,不跨线程直接操作界面。新 Ability 使用自己的 Context,首页关闭后文件选择仍可继续。DocumentStore 只保留 Picker、URI 授权打开及关闭;目录、时间戳、命名、最近使用和缓存规则由通用 C++ SandboxStore 负责;鸿蒙授权 fd 复制与适配由 HarmonySandbox 负责。
仓库中的 third_party/markitdown/source 固定源码版本,不会把整个第三方 Git 仓库装进应用。pack_python_hnp.sh 只部署 Python 解释器、目标依赖及其原生 .so 和自检脚本;build_python_bundle.py 把 markitdown/、huiwen_converter/、固定转换入口及许可材料装入独立 .pyz,应用 third_party/markitdown/patches/no-magika.patch,不修改子模块源码。两个产物随同一个 HAP 分发;NAPI、Qt 应用和最小 Qt 运行库仍按原有方式部署。
EntryAbility 和 QtWorkspaceAbility 在启动期调用 configurePython(resourceManager, cacheDir)。C++ 只读取固定的 rawfile/python/huiwen_converter.pyz,通过系统 ResourceManager 取出字节,以 SHA-256 命名保存到 <应用 cacheDir>/huiwen-python/<sha256>.pyz;缓存字节相同时不重写,缺失或内容不符时先写临时文件再替换。旧任务持有的旧版本文件不会在初始化新包时被覆盖,缓存可随应用缓存一起清理。
转换继续使用 convertPdf(inputPath, outputPath) / RunPdfConversionResult(),由 HNP 的 bin/python3 -B -u <缓存 pyz> <PDF> <MD> 启动固定业务入口。Python 从 pyz 导入业务模块,从 HNP 的 site-packages 加载依赖和原生扩展;不回退到 HNP 中旧的转换源码,不接受外部脚本路径。pdf_to_md()、stdout 最后一行 JSON 协议、错误码和页面调用方式保持原有边界。自检仍执行 HNP 的 python_smoke.py。
| 位置 | 项目接口 | 调用者与职责 |
|---|---|---|
converter/huiwen_converter/__init__.py |
pdf_to_md(input_path, output_path) -> Path |
Python 公开业务接口;生成非空 MD 后返回路径,失败抛出异常。后续格式实现并验证后,才新增对应的显式接口。 |
converter/huiwen_converter/_markitdown_adapter.py |
内部 pdf_to_markdown() |
唯一直接导入 MarkItDown 的文件;不作为业务 API。 |
entry/src/main/ets/services/ConversionService.ets |
pdfToMd(inputPath, outputPath): Promise<string> |
ArkTS 页面调用;只转发到 NAPI,成功返回 MD 路径。 |
entry/src/main/cpp/napi_bridge.cpp |
configurePython(resourceManager, cacheDir) |
启动期部署应用内置 pyz;两个 Ability 都提供自己的 Context,不接受外部脚本路径。 |
entry/src/main/cpp/napi_bridge.cpp |
convertPdf(inputPath, outputPath) |
原生边界;异步用 HNP 解释器执行当前业务 pyz,不接受任意 Shell 命令。 |
sandbox/SandboxStore.h/.cpp是 Qt Core 通用实现,负责preview、edit、convert三类目录、任务命名、最近记录、编辑缓存查询和目录归属校验,不引入 JSON、数据库或额外索引文件。鸿蒙把它编入libhuiwen_native.so,Qt 工作区链接复用;桌面直接编译此实现。SandboxService仍是 Qt 界面入口;鸿蒙通过 ArkTS Picker 获得授权,桌面使用 Qt 选择器。输入统一原子保存,失败只清理本次输入及空任务目录,不覆盖其他任务。platform/harmony/harmony_sandbox.h/.cpp负责鸿蒙 fd 复制;NAPI 同步复制授权 fd 并设置子进程不继承,在工作线程以独立偏移分块读取,成功、失败和排队失败都释放 fd。harmony_sandbox_napi.cpp的目录扫描也在工作线程运行。harmonyImportDocument(filesDir, name, fd, kind, mode)返回Promise<HarmonySandboxDocument>;mode为preview、edit、convert,取代原有布尔转换标记。支持 PDF、MD/Markdown、Word 导入归档;当前正式转换接口仍仅接 PDF→MD,DOCX 已接入只读预览,旧版 DOC 仍仅支持归档;Word 编辑与转换尚未开放。harmonyValidateOutput(filesDir, path)接受新任务的convert|edit/<任务>/output/*.md,兼容历史outputs/下的文件;仍检查真实路径、目录归属及非空可读 MD。历史数据保留原位,不批量迁移。harmonyValidateTaskDirectory(filesDir, path)直接复用SandboxStore::validateTaskDirectory(),只接受真实可读且不含符号链接或路径别名的edit|convert/<任务>/input|output目录。与 MD 文件校验分开,因此选取 PDF 后即可请求打开空输出目录;鸿蒙经openDirectory工作区事件调用系统文件管理器,桌面使用QDesktopServices。
下图对应当前源码中两侧均显示正文预览时的结构。外层表示平台承载,内层表示 Qt 控件与画面的视觉包裹;* 标注的 CEF Surface 只表示画面位置,其实际原生父窗口见图后说明。本次通用化重构仅完成格式检查,不代表已重新完成编译、打包或设备验收。
┌─ 鸿蒙系统窗口:QtWorkspaceAbility / WindowStage ────────────────────────────────────────────────────────────────┐
│ ┌─ Index.ets:XComponent NODE / QPA ──────────────────────────────────────────────────────────────────────────┐ │
│ │ ┌─ ConversionWindow : QWidget ────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ FunctionBar:[ 文件 ] [ 滚动设置 ] │ │ │
│ │ │ ┌─ WorkspaceSplitter ─────────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ┌─ DocumentView A : QStackedWidget ────────────┐ ║ ┌─ DocumentView B : QStackedWidget ────────────┐ │ │ │ │
│ │ │ │ │ 当前页:PDF 预览 │ ║ │ 当前页:MD 预览 │ │ │ │ │
│ │ │ │ │ ┌─ PdfPreviewWidget ───────────────────────┐ │ ║ │ ┌─ MarkdownPreviewWidget ──────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 继承 RasterDocumentViewport(同一控件) │ │ ║ │ │ 位置、尺寸及生命周期控制 │ │ │ │ │ │
│ │ │ │ │ │ ┌─ viewport : QWidget ───────────────┐ ▐ │ │ ║ │ │ ┌─ CEF 原生 Surface* ────────────────┐ │ │ │ │ │ │
│ │ │ │ │ │ │ ┌─ createWindowContainer ────────┐ │ ▐ │ │ ║ │ │ │ MD 正文 │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ ┌─ RasterRenderWindow ───────┐ │ │ ▐ │ │ ║ │ │ │ 浏览器绘制、滚动及惯性 │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ PDF 正文 │ │ │ ▐ │ │ ║ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ 独立 QBackingStore │ │ │ ▐ │ │ ║ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └────────────────────────────┘ │ │ ▐ │ │ ║ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └────────────────────────────────┘ │ ▐ │ │ ║ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ └────────────────────────────────────┘ ▐ │ │ ║ │ │ └────────────────────────────────────┘ │ │ │ │ │ │
│ │ │ │ │ └──────────────────────────────────────────┘ │ ║ │ └──────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────────┘ ║ └──────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ [ 转化 ] ← 悬浮于分隔条下部 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ 状态提示 QLabel │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- 平台承载:
QtWorkspaceAbility管理鸿蒙系统窗口;Index.ets的XComponent NODE通过 QPA 承载 Qt。ArkTS 首页属于另一个 Ability,不是工作区下面的一层背景。 - 工作区:
ConversionWindow组装FunctionBar、WorkspaceSplitter和状态提示。ConvertButton是主窗口的直接子控件,图中只标出悬浮位置;它不占分栏布局空间,按需显示并位于正文和分隔条之上。 - 共用区域:两侧都是
DocumentView : QStackedWidget,本身就是页面切换容器,不再额外包一层业务 QWidget。工作区分别注入 PDF 和 Markdown 渲染器,并接收文件卡片、打开完成与滚动信号;两侧不互相调用。 - PDF 预览:
PdfPreviewWidget继承RasterDocumentViewport,两者是同一个控件,不是父子包裹的两层。其viewport内的createWindowContainer()嵌入RasterRenderWindow,后者使用独立QBackingStore绘制。图中▐是位于正文视口外侧的DocumentScrollBar。 - Markdown 预览:
MarkdownPreviewWidget与WordPreviewWidget继承共用CefDocumentPreviewWidget,各自仍是一个 Qt 占位及控制控件。CEF 的实际原生父窗口是ConversionWindow对应的 Qt 顶层窗口,按占位区域映射位置与尺寸,并使用自己的 Surface;它不是普通 QWidget 子画布。 - 非可视能力:
DocumentRenderer是 C++ 渲染接口;DocumentScrollController是 Qt 滚动能力对象;QBackingStore和页面缓存是内部数据/绘图对象。这些都不构成额外可视壳层。 - 能力配置:
DocumentView::setViewOptions()接收DocumentViewOptions,默认启用inertiaEnabled和scrollBarVisible。前者只控制 Qt 侧追加的惯性;CEF 保留引擎自身的手势处理,只映射滚动条显隐。scrollOwner()区分 View 管理滚动与 Backend 管理滚动,不在 CEF 外再加 Qt 滚动区域。 - 复用边界:卡片与预览切换不依赖输入/输出角色。工作区按
kind注入 PDF、MD 或 Word 渲染器,卡片本身仍只显示图标和文件状态;Word 单栏只读预览沿用同一契约,不创建 PDF/MD 转换结果区。 - 画面隔离与通信:PDF 和 MD 正文分别使用独立画面缓冲。此前独立 PDF 缓冲方案获得用户真机无拖动残影的反馈,本次重构仍需重新验收;这也不代表模拟器 CEF GPU 崩溃已解决。QPA 负责窗口、绘制和输入适配,NAPI/
QtBridge负责文件选择、任务恢复与返回首页等业务消息。
切到占位图模式或尚无文件时,DocumentView 显示另一页;两页互斥显示,卡片的滚动区域不会包在 PDF 或 CEF 正文外侧:
┌─ DocumentView:卡片页(两侧共用) ─────────────────────────────────────┐
│ QScrollArea → viewport → 内容 QWidget → FileCard │
│ 显示图标、文件名或选择提示;正文预览页隐藏 │
└────────────────────────────────────────────────────────────────────────┘
顶部灰色功能条由 FunctionBar 组装,FileMenu 只维护菜单状态、发出请求,ConversionWindow 负责协调预览和沙箱服务;两侧区域不持有“预览/返回文件、更换文件、打开所在目录”工具按钮。SelectionMenu 统一绘制右侧 #3983F7 蓝点(直径 14px,圆心距菜单项右边缘 24px),子菜单箭头放在更靠右的位置,文件菜单与滚动设置保持对齐。
文件
├── 查看模式
│ ├── 占位图模式
│ ├── 预览模式(默认)
│ └── 编辑模式(禁用,TODO)
├── 文件目录
│ ├── 输入目录
│ └── 输出目录
└── 切换模式
├── 预览模式(禁用,TODO)
├── 编辑模式(禁用,TODO)
└── 转化模式(当前,右侧蓝点)
└── 更换文件
- 查看模式同时控制左右区域,进入工作区和恢复任务时默认选中预览;未选择 PDF 时仍显示选择入口,未生成 MD 时右侧继续显示等待提示。选择新文件和开始转换保留当前查看方式,转换成功自动切回预览,并同步菜单蓝点;“切换模式”中的业务模式始终为转化。
- 仅当前“转化模式”拥有更换入口;取消保留原任务、结果、查看方式和按钮状态,选取成功创建新任务、清空旧结果并恢复转化按钮。按钮显隐直接读取
DocumentView::hasDocument()(结果区域实例),不依赖 CEF 加载完成,不重复维护完成状态。选择或转换期间禁用查看方式切换与更换文件,避免任务串线。其他业务模式和编辑视图只保留 TODO,没有接入业务切换。 - 输入、输出目录均定位当前任务下的目录,未选择 PDF 时禁用;系统文件管理器是否准确定位仍需真机确认。原 MD 文件卡片的目录跳转继续使用非空文件校验。
- “滚动设置”的菜单选中项直接控制联动,不再保留隐藏复选框作为第二份状态。默认“同步滚动”:左右预览均就绪时,任意一侧滚动带动另一侧,优先按 PDF 页码对齐,无页码时按总进度同步;切回同步时按当前 PDF 位置对齐一次。“单向滚动”:只滚动当前操作区域,不向另一侧转发位置。更换文件、恢复任务和切换查看方式沿用当前滚动设置。本次接线仅做格式检查,切换效果仍需设备验收。
分隔区和滚动条外观放在独立组件中:
WorkspaceSplitter独立封装分隔区,沿用 Qt 原生拖动。普通分隔线为 1px;按住拖动时主线为 5px,颜色#546987,鼠标 Y 坐标处显示宽 10px、长 180px、圆角半径 5px 的同色指示条,中心随鼠标上下移动,松开后恢复普通状态。命中区为 11px,容纳加宽的指示条;按下或松开时不因线条变粗改变布局。- 鸿蒙分隔条使用独立原生子节点;分隔条显示、开始拖动和 PDF/MD 就绪时显式提升到 CEF 之上,移动时沿用该层级,避免每个鼠标事件都等待一次 ArkUI 置顶调用。覆盖范围仅限自身 11px 区域;悬浮转化按钮仍可覆盖分隔条。其他平台沿用 Qt 兄弟控件层级。
DocumentScrollBar只维护文档竖向滚动条外观:宽 24px、滑块最小高度 30px、常态#c6c6c6、无圆角、透明轨道且无上下箭头。公共RasterDocumentViewport安装滚动条后,再由DocumentScrollController绑定动画;PDF 后端处理页码定位,两侧同步由工作区协调。MD 继续使用 CEF 滚动条。
PDF 查看器继续使用 PDFium 与 Qt Widgets;PdfPreviewWidget 负责页面、缓存和同步定位,公共栅格视口负责绘图承载,DocumentScrollController 负责 Qt 输入与惯性:
- 有像素位移时即时跟手;鸿蒙触控板的物理像素按窗口像素比换算为逻辑像素,并修正方向,避免高分屏放大滑动速度;鼠标不取反,Windows/macOS 沿用 Qt 像素方向。换算后的不足一像素位移累计到下次,不因取整丢失慢速输入。只有角度位移的滚轮沿用 100ms 短动画。
- 触控板记录近期位移与时间,松手后按末速度匀减速续滑,最长 600ms;慢速、停顿或到达边界时不续滑。零位移的
ScrollEnd也会处理,并等待 32ms 接收系统惯性;收到ScrollMomentum时取消自有惯性,避免叠加。再次触碰、反向滑动、操作滚动条、对侧同步定位、换文件、调整尺寸或隐藏预览会中止旧运动。 components/scrolling/DocumentScrollController.cpp中的touchpadSpeed(默认 1.0)和touchpadDeceleration(默认 0.005 逻辑像素/ms²)集中控制速度与减速。当前已统一位移单位;与右侧 CEF/MD 的实际续滑距离、时长及速度仍需真机对比校准,不能将默认参数视为与浏览器手感完全一致。- PDF 正文通过通用
RasterRenderWindow的独立QWindow + QBackingStore绘制,由RasterDocumentViewport使用createWindowContainer()嵌入视口。原生窗口将滚轮、触控板阶段和键盘事件转交公共视口;Qt 滚动能力由DocumentScrollController提供,PDFium、页面缓存与页码定位留在 PDF 后端。滚动复用独立缓冲中的像素,仅补画新露出的条带;后台页面图片完成后仍只刷新对应区域。关闭时先解除绘制和输入回调,再等待后台任务完成。 - PDF 原生容器只创建自身节点,避免连带把整个分栏提升为不透明原生窗口;PDF 或 MD 原生预览显示后,工作区统一恢复分隔条和悬浮转化按钮的层级。鸿蒙日志
HUIWEN_RASTER_SURFACE backend=independent-raster created直接写入 HiLog,用于确认新容器已构造。2026-10-30 用户反馈此前独立缓冲方案真机拖动已无残影;本次通用化重构仅做格式检查,未新增编译、打包或设备验收。 - 拖动分隔条或调整窗口时,PDF 每约 16ms 合并一次最新布局与滚动同步请求,实时绘制缓存位图并临时关闭平滑缩放;尺寸停止变化 150ms 后恢复平滑绘制并请求高清页面。换文件或隐藏预览时取消待处理的布局与缩放任务。MD 保持原有实时更新,未增加人为延迟。调整尺寸期间不重新解析 PDF。
- CEF 原生窗口的几何更新只保留一个待处理任务,尚未执行的请求合并为最新位置和尺寸;实际应用时跳过重复状态与空尺寸,关闭后的任务不再操作原生节点。仅变化的属性交给 ArkUI/XComponent 更新,不再调用离屏渲染专用的
WasResized()、WasHidden()或限 Windows/Linux 的NotifyMoveOrResizeStarted()。此调整用于收敛窗口更新;模拟器之后仍出现同地址的 CEF GPU 崩溃,不能将其视为崩溃已修复。 - 保留必要错误日志:
HUIWEN_CEF stage=geometry_failed表示 CEF 原生节点更新失败,HuiwenCEF记录引擎及页面生命周期;公共栅格绘图窗口构造时输出一次HUIWEN_RASTER_SURFACE。临时逐次拖动和绘制采样日志已移除。 - 优先后台串行渲染当前可见页,再在 64 MiB 缓存预算允许时预取下一页、上一页;没有余量时跳过预取。每页采样预算按布局中可能同时可见的最多页数确定,不随滚动位置变化,避免跨页导致缓存尺寸反复失效。超长页仍降采样,不做整份文档预加载或分块渲染。
- 触控板、鼠标滚轮、跨页及同步/单向滚动需按设备逐项验收;真机残影通过的反馈不等于 Windows/macOS 或模拟器验收通过。
鸿蒙根目录取自 getApplicationContext().filesDir;Windows/macOS 使用 Qt 的 AppDataLocation。设备实际物理映射仍以运行时为准,以下仅展示 files/ 下的规则:
files/
├── preview/
│ └── 20261024_173015_报告/
│ └── 报告.pdf
├── edit/
│ └── 20261024_173015_报告/
│ ├── input/
│ │ └── 报告.docx
│ └── output/
│ └── 报告.docx
└── convert/
└── 20261024_173015_报告/
├── input/
│ └── 报告.pdf
└── output/
└── 报告.md
- 任务目录名使用设备本地时间
yyyyMMdd_HHmmss_文件名(不含扩展名);同秒同名以(2)、(3)区分,输入输出共用此目录。普通文件名保留原样;非法路径字符拒绝,跨平台文件名特殊字符替换为下划线,过长文件名明确报错;仅目录中的文件名部分按 UTF-8 长度截短。 - 预览直接保存文件;编辑、转化创建
input/和output/。导入时仅生成输入副本及预定输出路径,输出文件在实际保存或转化时生成。失败重试沿用当前任务目录,重新选取文件创建新目录。 - 目录名时间表示创建时间,任务目录的修改时间表示最近使用时间。导入完成、PDF 成功打开、转换开始及结束都会显式更新任务目录修改时间;时间写入失败记录
HUIWEN_SANDBOX stage=touch_failed,不会把已经成功的转换改判失败。扫描列表和查询缓存本身不更新时间,也不修改目录名。 - 最近任务只扫描三类目录的一层任务子目录,合并后按
lastUsedAt(毫秒时间戳)降序,时间相同时按任务目录名排序。跳过无输入、失效目录、越界符号链接或不可读缓存;旧目录继续支持输出定位,但不伪造旧任务记录。 - 扫描历史编辑任务时,
openPath优先指向已有、可读的output/原文件名,尚无输出时指向input/原文件名;输出已存在但无效时跳过该任务。编辑目录及扫描兼容保留,当前不提供未使用的同名缓存查询接口。
ArkTS 入口(utils/DocumentStore.ets) |
用途 |
|---|---|
| `pickDocumentToSandbox(context, kind, DocumentMode.Preview | Edit |
getRecentDocuments(context, limit = 5, mode = 'all') |
后台读取最近任务,数量为 1–100;可筛选任一类型 |
这些接口通过 NAPI 调用同一份 SandboxStore,返回名称、格式、模式、taskPath、inputPath、outputPath、openPath 和 lastUsedAt;选取成功直接返回导入结果,不再额外复制字段或附带未使用的 URI。Qt 侧继续使用 SandboxStore::recentDocuments()、markUsed(),在实际导入、打开或转换后更新使用时间。首页已接最近工作区列表与预览工作区,编辑入口仍只展示格式选项。
- 右侧主区顶部保留“开始使用”和三个模式入口,顶部留白 44vp;提示文案为“请选择转化模式”,与三个按钮区域的间隔为 84vp(原 24vp 间距加按钮区顶部 60vp)。上区使用普通
Column按内容高度排列,不使用Scroll或纵向layoutWeight(1);父容器顶部排列且间距为 0,最近工作区通过自身顶部外边距与上区保持 50vp 距离,不再贴底。16vp 标题在记录滚动区外固定显示;窗口大小及左侧介绍区比例不变。 RecentWorkspaceService.listRecentWorkspaces(context, limit = DEFAULT_RECENT_WORKSPACE_LIMIT)默认取三条,数量常量为3,底层仍支持 1–100。扫描preview/edit/convert三类任务,按目录修改时间降序排列,不创建 JSON、数据库或新任务目录。- 每行高度为 48vp,模式图标底框 28vp、内部 SVG 18vp;文件名、格式、时间字号分别为 14、12、11,加载、空列表及失败提示为 12。格式列为 104vp、时间列为 152vp,格式与时间间隔 8vp。文件名使用
layoutWeight(1)占满剩余宽度,单行中部省略,鼠标悬停显示全名;固定列不参与收缩。时间显示设备本地yyyy-MM-dd HH:mm,使用灰色文字。 - 仅记录区域使用
Scroll,标题不进入滚动容器;列表高度按条数计算并以maxListHeight限制,默认最大 240vp,可由调用方传参调整。默认三条时高度为 160vp,单条为 48vp,不补空行;更多记录超过最大高度后在内部滚动。 - 行点击传递最近列表中的
taskPath、mode和kind,首页据此填入 Want 参数并启动对应工作区,跳过格式弹窗和选择器,不再提前读取任务。Qt Ability 统一经一次异步harmonyLoadTask(filesDir, taskPath)调用SandboxStore::loadTask()做最终磁盘校验,支持 PDF/MD/Word 预览及 PDF→MD;实际输入输出路径从磁盘重读,不使用页面缓存的文件路径。列表元数据已失效或与磁盘类型不符时拒绝恢复,窗口侧仍保留类型和可读性检查。 SandboxService::restorationRequested()通知ConversionWindow::restoreDocument();窗口正在选择或转换时拒绝替换,原 PDF 失效时也不覆盖当前任务。恢复时切到预览,加载原 PDF 和非空的已有 MD,并隐藏转化按钮;MD 不存在或为空时继续显示等待提示并保留按钮,不自动重新转换。- Qt 载入后调用
SandboxService::finishRestoration()更新时间,通过restoreResult回执通知 Qt Ability,成功后才发出首页关闭通知。读取列表、恢复前校验及恢复失败不更新时间;目录名的创建时间不变。新启动及onNewWant复用窗口都走同一恢复链。日志前缀为HUIWEN_QT_RESTORE_OK/HUIWEN_QT_RESTORE_FAILED;系统实际交接与显示仍需真机验收。 - 首页查询和恢复前校验可能先于
QApplication执行;任务命名校验使用字符检查,PDF 预读使用 C++ 标准文件流,避免在 NAPI 工作线程通过QFile(QObject)或正则 JIT 提前初始化 Qt 线程数据,导致工作区鼠标事件触发主线程断言。 - 启动顺序回归检查独立于 PDFium:
cmake -S qt_app/tests/sandbox_startup -B /tmp/huiwen-sandbox-startup -DCMAKE_PREFIX_PATH=<宿主Qt5路径>,然后cmake --build /tmp/huiwen-sandbox-startup,最后QT_QPA_PLATFORM=offscreen /tmp/huiwen-sandbox-startup/sandbox_startup_check。检查先在线程中查询、校验任务,再创建QApplication并分发鼠标事件;它不能替代鸿蒙真机恢复验收。
当前 Qt PDF→MD 选择链改为传递 DocumentMode.Convert,使用 convert/<任务>/input 与 output。转换成功才展示 MD 卡片,扫描件仍按原有错误提示处理;Python 继续接收具体输入、输出路径。点击 MD 卡片仍通过 openLink('filemanager://openDirectory') 传入校验后的结果 URI,打开真实输出目录,不创建应用内文件浏览器。
统一使用 .native-tools/design/huiwen-icon-rounded-final.png 的最终版图标,原图为 1024×1024 PNG,保留透明圆角。正式资源直接随仓库保存,构建使用以下文件:
AppScope/resources/base/media/app_icon.png:应用级图标,由AppScope/app.json5引用。entry/src/main/resources/base/media/app_icon.png:ArkTS 首页与 Qt 工作区 Ability 图标,以及各自的启动页图标。qt_app/resources/branding/app_icon.png:Qt 窗口图标,通过现有 QRC 注册为:/branding/app_icon.png,在main.cpp中统一设置。
三份资源均直接复制最终版 PNG;更新品牌图标时同步替换这三份文件。
工程当前配置为 HarmonyOS PC、arm64-v8a,目标 SDK 为 6.1.1(24)。首次克隆后,将 build-profile.example.json5 复制为本机的 build-profile.json5,需要安装到设备时再在 DevEco Studio 配置签名。
macOS ARM64 开发机首次 Run 会按 third_party/prebuilt/manifest.json 下载、校验并恢复固定的
Qt/CEF .so、头文件、CMake 配置、宿主工具和浏览器核心资源;后续 Run 校验本地文件后复用。
下载地址对应的依赖包须先发布,离线机器可通过 HUIWEN_PREBUILT_ARCHIVE 指定同一压缩包。
包哈希或本地依赖不匹配会停止构建,不覆盖已修改的库。详见 固定依赖包。
维护者准备或升级 Qt SDK 时,仍可导入同一套鸿蒙 Qt 5.15.12 开发文件和五个运行库:
python3 third_party/qt/prepare.py \
--source /绝对路径/Qt5.15.12-ohos17-arm64-v8a \
--license-source /对应的/qtbase源码目录--source 也可由原有 qt.sdk.dir 或 QT_PREFIX 指定,但它们只用于准备/升级依赖,正常 CMake 构建使用项目内 third_party/qt/。库的唯一部署位置是 entry/libs/arm64-v8a/,Qt 开发包内通过相对符号链接引用;宿主工具必须匹配构建电脑。修改 Qt 内核后需先编译新的 SDK,再执行准备命令同步整套依赖。
当前 PDF 残影修复使用应用内独立 QBackingStore,不要求修改或重新编译 Qt SDK。
PDFium fork 为 xristosstarnetgames439-del/pdfium 的 huiwen-dev 分支,首次同步提交记录官方源码 SHA;主项目固定子模块提交。首次构建会准备 DEPS 锁定的 Git 依赖、GN 和 Clang,之后由 Ninja 增量编译当前工作树,不重置用户的源码修改:
git submodule update --init third_party/pdfium/source
python3 third_party/pdfium/build.py --sync脚本使用鸿蒙 NDK sysroot、归档器和链接驱动,生成 aarch64-linux-ohos 动态库;内部 libc++ 静态链接,关闭 V8、XFA、Skia 和 PartitionAlloc,保留公开渲染、编辑及保存接口。通过 ELF 架构、必要导出和动态依赖检查后,原子更新 entry/libs/arm64-v8a/libpdfium.so,同时生成源码/构建参数/校验值记录及分发许可材料。构建失败不会覆盖有效旧库,但会阻止新 HAP 生成。可设置 OHOS_NDK_HOME 指向 native/。
DevEco Run 的顺序为 PreBuild → PrepareNativeDependencies(恢复固定 Qt/CEF,再增量构建 PDFium)→ PrepareMarkdownResources → ConfigureCmake / CompileResource → BuildNativeWithNinja → ProcessLibs → PackageHap → RepackHapWithHnp → SignHap。PDFium 编译和 Qt/NAPI 库同步在收集原生库之前完成;未改动的源码复用产物。这里只实现运行构建时更新,没有文件监听器,也没有在运行进程内卸载和替换 C++ 库。
Markdown 另由 PrepareMarkdownResources 在 PreBuild 后、ConfigureCmake、CompileResource 和 PackageHap 前准备。
同一资源准备任务还使用宿主 Python 执行 build_python_bundle.py:收集当前转换代码与许可、应用补丁、固定 zip 成员顺序/时间/权限;新旧 pyz 字节相同则输出 [HUIWEN_PY_BUNDLE] CACHED 并保留时间戳,否则原子替换并输出 GENERATED。不复制运行环境或目标 wheel,也不将本机字节码和原生扩展放入 pyz。
它复用 Hvigor 的 Node/npm,首次 Run 联网按锁文件自动安装依赖,之后每次构建并比对
entry/src/main/resources/rawfile/markdown/ 下的 JS/CSS、页面、许可证和校验清单;仅原子替换内容变化的文件,其余保留时间戳,避免 Qt RCC 无意义重编。全部相同时输出 [HUIWEN_MD] CACHED,有更新时输出 GENERATED,资源随 HAP 收集;
安装或构建失败会停止打包。通用预览模板还经 Qt qrc 编入工作区动态库,直接接收 MD 原文,
不为每篇 MD 另存 HTML。CEF 运行库使用项目内预编译版本,仅其包装层参与 C++ 编译;
轻量 HAR 模块已固定在 third_party/cef/web_engine/,OHPM 首次同步无需等候大依赖下载。
Python HNP 的运行时路径仍按下文配置,本次 Qt/CEF 自动准备不替代该配置。详见 Markdown 构建说明
和 CEF 环境说明。
需要核对更新链路时,可运行 python3 packaging/ohos/check_run_update.py。它临时向 PDFium 加入两版构建探针,分别执行 HAP 构建并核对 libs 与 HAP 中的实际库内容,最后移除探针并重建最终包;不会安装或启动设备应用。该检查需要已配置可用的 HAP 签名,也会执行原有 Python HNP 打包。
首次克隆时连同固定版本的 MarkItDown 子模块一起下载:
git clone --recurse-submodules https://github.com/xristosstarnetgames439-del/HuiwenMCP.git HuiWenMCP
cd HuiWenMCP
git submodule status third_party/markitdown/source已经克隆了主仓库的开发者,在仓库根目录补拉子模块:
git submodule update --init third_party/markitdown/source
git submodule status third_party/markitdown/source再取得鸿蒙 arm64 Python 3.14 的已解压安装目录,要求包含 bin/python3 和 lib/python3.14/site-packages/。原有自检使用 Harmonybrew 的预编译 Python 3.14.6 通过。在被 Git 忽略的 local.properties 中配置本机路径:
python.runtime.dir=/绝对路径/python-3.14.6-ohos-arm64
# 可选:指定本机带 pip 的 Python,避免 DevEco 的 PATH 找不到解释器。
python.host.path=/绝对路径/宿主/python3也可用 HUIWEN_PYTHON_RUNTIME 和 HOST_PYTHON 环境变量覆盖上述配置。相对配置路径按项目根目录解析。正常点击 DevEco Run 并执行 HAP 构建安装时,签名前的 RepackHapWithHnp 任务先检查 HNP 输入,未变化时复用成品;首次准备、输入变化或产物缺失/校验不符时重新生成。仅热重载不保证执行此任务。也可手动执行同一检查和打包流程:
python3 packaging/ohos/build_python_bundle.py
bash packaging/ohos/pack_python_hnp.sh /绝对路径/python-3.14.6-ohos-arm64脚本使用本机 Python 的 pip 下载 requirements-pdf.txt 中锁定的鸿蒙 arm64/musl wheel。目标依赖按清单 SHA-256 缓存在被 Git 忽略的 .hnp-cache/,清单变化时重新安装。环境成品指纹保存在 .hnp-cache/hnp-build.sha256,只覆盖运行时目录、目标依赖清单、自检脚本、hnp.json、环境打包脚本和 hnpcli;按文件内容、路径、权限及符号链接目标计算,并校验已有 HNP 的 SHA-256。汇文 API、MarkItDown、转换入口和业务补丁的修改只影响独立 pyz;ArkTS、Qt 和 PDFium 修改也不会使环境 HNP 失效。
需要重建时只复制运行时与目标依赖,清除运行时目录中可能残留的汇文 API、MarkItDown 和旧转换入口,再用 SDK 的 hnpcli 打包;成功后原子替换 hnp/arm64-v8a/huiwen_python.hnp,再写入新指纹。任一步失败仍停止构建。删除 .hnp-cache/hnp-build.sha256 或成品 HNP 可强制重打包;删除整个 .hnp-cache/ 可连同目标依赖一起重新准备。首次迁移到环境/业务拆分会重建一次 HNP,之后仅修改业务代码不会重建环境。若 SDK 工具不在默认位置,可设置 HNPCLI=/绝对路径/hnpcli。HNP 的 name/version 固定为 huiwen_python/1.0.0。
MarkItDown 官方源码保持在子模块的 v0.1.8 提交;它采用 MIT 许可,可修改并商用,但分发时需保留许可与第三方声明。其官方包强制依赖 Magika/ONNX Runtime,而当前目标架构无法直接解析 ONNX Runtime wheel。仓库中的补丁只让 Magika 可选;本阶段输入已由选择器限定为 .pdf,按扩展名交给 PDF 转换器。补丁不改动子模块原始源码。pip 能解析目标架构 wheel 只说明可获取包,不代表鸿蒙设备能加载其中的原生 .so;需在设备上完成导入及实际 PDF 转换验收。
Hvigor 的默认 PackageHap 不会自动带入根目录的 HNP。entry/hvigorfile.ts 在 PackageHap 后、SignHap 前先准备 HNP,再调用 SDK app_packing_tool.jar --hnp-path 重打未签名 HAP,并检查环境 HNP 和 resources/rawfile/python/huiwen_converter.pyz 均存在;复用环境不跳过注入和签名。构建日志中的 [HUIWEN_HNP] CACHED/GENERATED 与 [HUIWEN_PY_BUNDLE] CACHED/GENERATED 分别表示两个产物的状态。SDK 不在默认位置时设置 DEVECO_SDK_HOME 或 OHOS_SDK_HOME。设备日志筛选 HuiwenPython:PYTHON_BUNDLE 显示缓存业务包哈希及路径,PYTHON_START 显示 HNP 解释器与当前 pyz,RUNTIME 显示实际导入模块及算法标识。
本次拆分仅做语法和格式检查,未执行构建、打包或设备运行。后续验收:首次迁移允许环境 HNP 和业务 pyz 各生成一次;原样复跑应同时命中缓存;只改 Python API 或 MarkItDown 时应仅见业务包 GENERATED、环境 CACHED;改目标依赖清单才应重建环境。安装后确认 PYTHON_START 的脚本来自缓存 pyz,实际完成一次 PDF 转换;可再运行 packaging/ohos/check_workspace_hap.py <signed.hap> 检查 pyz 入口和许可。对比构建耗时,安装和无线传输另行计量;HAP 仍携带完整环境,并非设备差量安装。
cd entry && ohpm install
cd ..
hvigorw assembleHap --mode module -p product=default -p module=entry@default也可以在 DevEco Studio 中使用 Build Hap(s)/APP(s)。修改代码后至少执行 git diff --check;需要验证设备行为时,再安装生成的签名 HAP 并启动 EntryAbility。Python 运行时自检已在真机通过;后续若更换运行时或 HNP 内容,可临时调用保留的 checkPython() 再验证。本机签名文件已被 Git 忽略,推送前用 git status 确认它没有进入提交。
- 新增功能时更新“当前状态”,区分已运行的能力与尚未接入的入口。
- 调整模块、原生库或打包方式时更新“工程结构”和“本地构建”。
- 不把生成的 HAP、构建缓存或本机 SDK 路径写成跨机器通用的依赖。
- 不提交
entry/libs/和third_party/qt/中的本机开发依赖;NAPI、Qt 应用与 PDFium.so按源码增量生成,Qt 运行库由准备脚本导入。主仓库只固定 PDFium 子模块提交,fork 修改在其独立仓库维护。
- 窗口大小和窄侧栏保持原有设置。除侧栏、外边距及分区间距外,介绍区与主区域使用
layoutWeight(3)/layoutWeight(7)分配宽度,主区域取消固定 360vp 宽度。 - “开始使用”下三个入口保持等宽列布局,浅灰正方形边长为所在列宽的 50%,即上一版方块边长的一半;方块内只放居中的 SVG,文档预览、文档编辑、文档转化名称以普通字重显示在方块外下方。
HomeFeatureCard只负责显示和点击事件,HomePage保存当前功能选项。 - 点击入口后,按
docs/自定义弹窗模版.md使用getUIContext().getPromptAction().openCustomDialog()打开居中的独立格式选择子窗口,采用showInSubWindow: true、isModal: false的组合(该接口禁止子窗口同时启用模态,也不支持CustomDialogController的customStyle参数);首页保存返回的弹窗 ID,通过closeCustomDialog(id)统一关闭,并处理打开失败、外部取消及首页退出期间的异步清理。系统背板与内容组件HomeFormatDialog共用HOME_FORMAT_DIALOG_WIDTH = 360,内外宽度统一固定为 360vp,不再使用 90% 限制;高度随内容。只由系统背板绘制浅灰背景、16vp 圆角和阴影,关闭背景模糊,内部保持透明,避免宽度不一致留下透明空白。三个弹窗共用内容布局,内容居中;左右及底部内边距为 24vp,顶部为 40vp,为右上角关闭按钮留位;格式选项自身内边距为 12vp,转换入口使用透明背景,不再额外叠加白底和边框。弹窗不在首页内展开或预留面板高度,右上角仅保留 28×28vp 关闭按钮,内含 18×18vp 的 × SVG,也可点击外部取消。预览窗口的 Word(DOCX)、PDF、MD 入口打开对应单栏 Qt 工作区,由工作区选择文件;编辑窗口仍只展示格式;转化窗口展示 PDF→MD 组合图标入口。点击该入口后由HomeFormatDialog.onConvertRequested通知首页,关闭子窗口成功后再启动现有 Qt 工作区,仍在 Qt 窗口就绪后结束首页 Ability。启动期间禁用重复操作,失败显示错误。 HomeFormatDialog通过title参数接收首页传入的标题,预览为“请选择要预览的格式”,编辑为“请选择要编辑的格式”,转化为“请选择要转化的格式”。标题以 18vp 普通字重显示在内容左上方,与格式选项间隔 16vp;保留右上角关闭按钮、360vp 内外统一宽度及原有背景设置。- 功能 SVG 使用 Feather
book-open、edit-3、repeat,转化方向使用arrow-right,弹窗关闭使用x,许可随rawfile/licenses/feather-icons-LICENSE.txt分发。格式 PNG 原样复用 Qt 已有图标,来源见 文件格式图标说明。所有图标从应用资源加载,无需运行时下载。
- 侧栏固定在启动首页左侧,宽度 60vp;底部仅一个三横线按钮,尺寸 36×36vp,距底部 20vp。图标为
resources/base/media/sidebar_menu.svg,来自 Feather Menu,保留三横线几何形状,颜色跟随首页外观,以 24×24vp 的Image显示;上游 MIT 许可 随 rawfile 分发。按钮保留零内边距,不依赖字符字体或文本省略行为。功能卡片显示在侧栏右侧的主区域。 EntryAbility在页面加载后,先设置setWindowBackgroundColor('#00000000'),再独立设置setWindowContainerColor('#00000000', '#FFF5F7FB');后者需要ohos.permission.SET_WINDOW_TRANSPARENT,已在模块中声明。焦点态容器透明,非焦点态使用不透明底色。两个步骤各自处理异常,设置失败时保留必要错误日志,不阻断首页使用。根布局透明,侧栏背景为#fae5e9ec(约 98% 不透明度),主内容背景仍为实色;只调整背景透明度,按钮保持清晰,不额外增加模糊效果。- 侧栏组件通过
FunctionSidebar.onMenuRequested打开首页菜单,并通过onMenuAnchorChanged同步按钮锚点;Qt 转换工作区不增加侧栏。
本阶段页面入口为 qt_app/src/workspace/ConversionWindow.cpp。未创建后续侧栏目录;新增侧栏时使用 sidebars/,避免与输入/输出区域混淆。
- 点击 ArkTS 首页“文档转化”,再点击独立格式选择窗口中的“PDF → MD”,格式子窗口关闭后打开 Qt;Qt 默认最大化并保留系统标题栏、窗口边框及窗口控制按钮。查看
QtWorkspace标签下的HUIWEN_QT_START、HUIWEN_QT_READY,确认只有EntryAbility随后结束,QtWorkspaceAbility保持运行;不应再出现newLocalStorage not found、Need object。 - 左右文档区域占满状态栏上方的可用高度,底部状态栏保持紧凑;还原窗口并调整大小时布局应同步变化。悬停中间细线出现水平分割光标;拖动改变两侧宽度,底部小型“转化”按钮保持居中在线上。
- 进入工作区应默认选中“文件 → 查看模式 → 预览模式”,业务仍是“转化模式”。点击左侧虚线加号选择 PDF,默认直接显示 PDF 预览;手动切到占位图时显示文件卡片。拖动分隔线或缩放窗口后,页面应按当前可用宽度重排。“文件 → 切换模式 → 转化模式 → 更换文件”应保留查看方式、清空旧 MD、恢复转化按钮,预览方式下从新 PDF 第一页开始。取消选择应保留原文件、MD、查看方式及按钮状态。选择或转换期间不得重复更换;损坏、加密文件应提示原因。两侧不再显示旧工具按钮。
- 点击“转化”,操作期间不能重复选择或提交。即使转换前处于占位图模式,成功后也自动切到预览并更新菜单蓝点,直接展示 MD、隐藏转化按钮,底部显示
code=0、成功提示和完整路径。失败显示错误码及处理建议,保留“重新转化”按钮;重试成功后同样隐藏。选择新文件后按钮恢复,取消选择保留原状态;切换查看方式、拖动分隔线及缩放窗口不能重新显示已有结果的转化按钮。 - 转换成功后底部显示完整 MD 路径;切到占位图模式后点击 MD 卡片,沿用系统文件管理器打开文件夹,文件夹内能看到真实 MD。不存在、为空或不可读取的结果应在跳转前明确报错,不能误报转换成功。
- 日志搜索
HUIWEN_OUTPUT:import_success为导入落盘路径,convert_start为输入/输出路径,verified为磁盘验证及字节数,convert_success为成功通知,locate_start为点击时重新检查,locate_requested为鸿蒙桥接已提交。系统跳转结果继续查看原有HUIWEN_QT_OPEN_REQUEST、HUIWEN_QT_OPEN_REQUEST_ACCEPTED或HUIWEN_QT_OPEN_FAILED。 - 新导入生成
convert/yyyyMMdd_HHmmss_文件名/input/原文件.pdf与output/原文件.md;同秒同名只对任务目录追加编号,文件名保持一致。首页默认展示最近三条记录,长文件名中部省略且不挤压固定列;点击应复用原任务、载入已有结果,不弹选择器、不新建目录。检查成功恢复、PDF 打开、转换开始及结束后任务目录修改时间会更新;查询和失败不更新时间。分别验证全新 Qt 窗口、已有窗口、转换中拒绝替换、PDF 被删除、尚无 MD 的任务。同一任务重试路径不变,历史outputs/md/继续支持原有目录跳转。 - 从最近工作区进入,应直接预览 PDF 与已有非空 MD,转化按钮隐藏;MD 不存在或为空时显示等待提示并保留按钮。点击系统标题栏关闭应回到首页并结束工作区 Ability。连续重复进入和关闭,包含 MD 仍在加载时关闭,确认无残留白窗、整体退出或 CEF 失效;每次进入仍最大化,首页在工作区载入成功后结束。
Python 固定入口在 stdout 最后一行返回 HUIWEN_CONVERT_RESULT {"code":0,"message":"转化成功,Markdown 文件已生成。","outputPath":"/沙箱/convert/时间戳_文件名/output/文件.md"}。失败时 code 非零、outputPath 为空、进程退出码为 1;详细异常写入日志,界面展示中文原因和处理建议。C++ 独立保存完整结果行,日志尾部缓存仍限制为 4096 字节,结果不受日志截断影响。成功仍保留 HUIWEN_CONVERT_OK 标记,原 ArkTS 接口继续成功返回路径、失败拒绝 Promise。
错误码:1001 参数或格式错误,1002 输入不存在,1003 加密或禁止提取,1004 PDF 损坏或不支持,1005 无可提取文字(暂不支持 OCR),1006 读写权限不足,1007 存储不足,1008 组件缺失,1099 其他转换异常;2001 原生进程或结果协议异常,2002 输出文件校验失败。C++ 同时验证字段类型、退出码、预期输出路径及真实非空文件,不能仅凭成功字段通知界面。
HAP 包结构校验:
python3 packaging/ohos/check_workspace_hap.py entry/build/default/outputs/default/entry-default-signed.hapWindows/macOS 的同一 Widgets 源码提供桌面入口,尚未编译验证和制作安装包;CMake 需通过 PDFIUM_LIBRARY 指定与宿主架构匹配的 PDFium 动态库。桌面转换需安装同一 Python 依赖、设置 PYTHONPATH 为仓库的 converter/,HUIWEN_CONVERT_SCRIPT 为 converter/pdf2md/convert.py 的绝对路径;可用 HUIWEN_PYTHON 指定解释器。
PDF 预览桌面回归检查(独立工程,不加入 HAP;需要桌面 Qt 5.15 和对应宿主架构 PDFium):
cmake -S qt_app/tests -B /tmp/huiwen-preview-check \
-DCMAKE_PREFIX_PATH=/桌面Qt目录 -DPDFIUM_LIBRARY=/本机PDFium动态库
cmake --build /tmp/huiwen-preview-check
QT_QPA_PLATFORM=offscreen /tmp/huiwen-preview-check/pdf_preview_check该检查生成两页彩色 PDF,验证导入后按钮、真实页面像素、多页滚动、调整宽度、更换文档与忙碌状态。鸿蒙 C++ 编译及 HAP 打包已通过。