欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_fontTools

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

这篇文章记录的是一次把 Python 字体处理三方库 fontTools 接入 HarmonyOS PC / 鸿蒙 PC 应用的完整过程。

fontTools 和很多桌面应用不一样,它本身不是一个现成的 GUI 软件,而是 Python 生态里非常常用的字体工具库。它可以读取字体信息、导出 TTX、把 TTX 编译回字体,也可以按文本生成只包含部分字符的小字体。也就是说,这次适配的重点不是“把一个窗口搬到鸿蒙 PC 上”,而是要解决一个更通用的问题:

鸿蒙 PC 应用如何调用 HNP Python 里的 Python 三方库,并把它做成真实用户可以操作的工具?

最终我们在项目里新增了一个 examples/harmony_pc/ 示例工程。这个工程不是命令行验证 demo,而是一个面向鸿蒙 PC 用户的字体处理工具:用户可以从文件选择器选择真实字体,查看字体信息,按输入文字裁剪小字体,导出 TTX,也可以选择 TTX 后再编译回字体文件。

在这里插入图片描述

一、项目背景:fontTools 是什么

fontTools 是 Python 生态中常用的字体处理库,很多字体工程、字体子集化、字体格式转换工具都会用到它。它能处理的内容比较多,比如:

  • 读取 TTF、OTF、TTC、OTC、WOFF、WOFF2 等字体文件;
  • 解析字体中的 nameheadhheaOS/2glyfcmap 等表;
  • 把字体导出成 TTX XML;
  • 把 TTX XML 再编译回字体;
  • 按文本、glyph 名称或 Unicode 码位生成子集字体;
  • 配合可选依赖处理 WOFF2、路径运算、XML 加速等能力。

普通用户最容易理解的能力是“小字体裁剪”。比如原字体里可能包含几千到几万个字符,但一个业务页面只需要显示“鸿蒙字体”这几个字。如果把完整字体打包进去,文件体积会比较大;如果只保留这几个字,就可以生成一个更小的新字体文件。

这次鸿蒙 PC 示例的核心能力就是把 fontTools 包装成四个独立模块:

  • 运行环境检查:确认 HNP Python 和 fontTools 能正常运行;
  • 选择原字体:从文件选择器选择真实字体文件;
  • 按文本裁剪小字体:输入要保留的文字,导出一个更小的新字体;
  • TTX 转换:把字体导出成 TTX XML,也可以把 TTX 编译回字体。

二、路线选择:不重写 fontTools,而是通过 HNP Python 调用

适配前先分析了几条路线。

第一条路线是把字体处理能力改写成 ArkTS 或 Native 实现。这条路线理论上更“原生”,但成本非常高。字体文件内部结构复杂,不同格式和不同字体表都有大量细节;如果直接重写,风险和工作量都不适合第一版适配。

第二条路线是保留 Python 三方库,让鸿蒙应用通过 HNP Python 调用 fontTools。这条路线更适合当前场景:fontTools 本身是成熟的 Python 库,鸿蒙 PC 上又可以准备 HNP Python 环境,那么应用侧只需要负责选择文件、组织参数、展示结果,真正的字体处理仍然交给 Python。

最终采用的是第二条路线:

鸿蒙 ArkTS 页面
  -> FontToolsHnpClient 写入 JSON 请求
  -> Native/N-API 模块启动 HNP Python
  -> fonttools_hnp_bridge.py import fontTools
  -> fontTools 执行字体任务
  -> Python stdout 返回 JSON
  -> ArkTS 解析结果并导出文件

这条路线的好处是:

  • 不需要重写 fontTools;
  • 可以复用 Python 生态里已经成熟的字体处理能力;
  • 适合 Python 三方库向鸿蒙 PC 工具化迁移;
  • 后续新增字体能力时,主要扩展 Python bridge 和 ArkTS client 即可;
  • Native 层只做桥接,不承担字体业务逻辑。

限制也比较明确:

  • 设备上必须有可用的 HNP Python;
  • HNP Python 环境里必须能 import fontTools
  • 带 native 扩展的可选能力需要单独验证;
  • 当前 Native 调用是同步启动 Python,生产项目里建议进一步放到后台线程或任务队列。

三、先确认 HNP Python 和 fontTools 能跑通

在写鸿蒙 UI 之前,第一步不是先画页面,而是确认目标设备上的 HNP Python 是否能正常启动,并且能导入 fontTools。这一点在三方库适配里很关键:这个 HAP 示例负责提供页面、文件选择器、Native 桥接和调用流程,但真正处理字体的仍然是设备里的 HNP Python 和 Python 三方库。

用户使用这个 demo 前,需要先准备下面这些条件。

1. 在鸿蒙 PC 上安装 Python 安装器

鸿蒙 PC 上不一定默认就有应用可调用的 HNP Python 环境。普通用户需要先打开鸿蒙 PC 的应用市场,搜索并安装 Python 安装器

安装完成后,再打开 Python 安装器,按页面提示完成 Python 运行环境安装。这个步骤完成以后,设备上才会出现本示例默认使用的 HNP Python 路径。

本次示例默认使用的 Python 路径是:

/data/service/hnp/python.org/python_3.12/bin/python3.12

2. 确认 HNP Python 能正常启动

可以在鸿蒙 PC 的 HiShell 或系统终端中执行:

/data/service/hnp/python.org/python_3.12/bin/python3.12 --version

如果能看到 Python 版本号,说明 HNP Python 基础环境已经准备好。如果命令提示文件不存在,通常说明 Python 安装器 还没有安装,或者安装器里还没有完成 Python 环境初始化。

3. 用同一个 HNP Python 安装 fontTools

这里要注意,不能只在开发机 macOS 上安装 fonttools,也不能随便换一个 Python 去安装。应用运行时启动的是鸿蒙 PC 上的 HNP Python,所以三方库也必须装到这个 HNP Python 环境里。

联网环境下可以执行:

/data/service/hnp/python.org/python_3.12/bin/python3.12 -m pip install -U fonttools
/data/service/hnp/python.org/python_3.12/bin/python3.12 -c "import fontTools; print(fontTools.__version__)"

这里还有一个容易写错的地方:安装包名通常写 fonttools,但 Python 代码里导入的是 fontTools,大小写不同。

如果设备不能直接联网,也可以提前在开发机上构建纯 Python wheel,再拷贝到鸿蒙 PC 上离线安装。fontTools 主体是 Python 代码,第一版建议先禁用 Cython 扩展,降低 ABI 适配风险:

python3 -m pip install build
FONTTOOLS_WITH_CYTHON=0 python3 -m build --wheel

然后在鸿蒙 PC 上使用 HNP Python 安装生成的 wheel:

/data/service/hnp/python.org/python_3.12/bin/python3.12 -m pip install /path/to/fonttools-*.whl

如果后续要处理 WOFF2、路径运算或 XML 加速等高级能力,还要根据 fontTools 的可选依赖继续安装对应包。这个示例的基础流程主要验证 TTF/OTF/TTC 字体信息读取、TTX 转换和子集字体导出。

4. 准备一个真实字体文件

这个 demo 不再内置测试字体,也不会强制用户走固定路径。用户需要自己准备一个真实字体文件,然后在页面里通过文件选择器选择。

常见可选文件类型包括:

.ttf
.otf
.ttc
.otc
.woff
.woff2

如果鸿蒙 PC 上暂时没有字体文件,可以从其他电脑拷贝一个用于演示的真实字体到鸿蒙 PC 的文档目录。后续点击 选择字体 时直接从文件选择器选它即可。

5. 在应用里按真实流程使用

环境准备好以后,用户在 demo 页面中的使用顺序是:

  1. 点击 检查环境,确认页面能显示 Python 和 fontTools 版本。
  2. 点击 选择字体,从系统文件选择器选择真实字体文件。
  3. 点击 查看字体信息,确认字体能被读取。
  4. 在“按文本裁剪小字体”模块输入要保留的文字。
  5. 点击 裁剪并导出小字体,通过保存选择器选择输出位置。
  6. 如果需要查看字体结构,再使用 导出为 TTX 文件
  7. 如果已经有 TTX 文件,可以先点击 选择 TTX,再点击 编译回字体

这样用户看到的是正常的文件选择和保存流程,不需要关心应用内部沙箱路径。

本地也提供了一个命令行验证脚本:

cd examples/harmony_pc
FONT_PATH=/path/to/your/font.ttf bash run_hishell.sh

这个脚本会依次验证:

  • version:检查 Python 和 fontTools 版本;
  • info:读取字体信息;
  • dump_ttx:导出 TTX;
  • subset:按文本生成子集字体。

如果这里失败,说明问题还在 Python 环境、依赖安装或字体文件阶段,鸿蒙应用里调用也不会成功。先把 HNP Python 侧跑通,后面的 ArkTS 和 Native 桥接才有意义。

四、新增鸿蒙 PC 示例工程

适配完成后,项目新增了鸿蒙 PC 示例目录:

examples/harmony_pc/
  AppScope/
  entry/
  hvigor/
  build-profile.json5
  oh-package.json5
  fonttools_hnp_bridge.py
  run_hishell.sh
  sample_request.json
  sample_subset_request.json

其中真正的鸿蒙应用工程就在 examples/harmony_pc。几个关键文件如下:

entry/src/main/ets/pages/Index.ets
entry/src/main/ets/fonttools/FontToolsHnpClient.ets
entry/src/main/ets/native/FontToolsHnpNative.ets
entry/src/main/ets/util/RawFileSync.ets
entry/src/main/cpp/napi_init.cpp
entry/src/main/cpp/types/libfonttools_hnp_bridge/
entry/src/main/resources/rawfile/python/fonttools_hnp_bridge.py

这些文件的职责比较清楚:

  • Index.ets:鸿蒙 PC 页面,负责选择字体、输入文本、选择导出位置和展示结果;
  • FontToolsHnpClient.ets:ArkTS 侧业务封装,把字体操作变成 JSON 请求;
  • FontToolsHnpNative.ets:声明 Native 模块方法;
  • RawFileSync.ets:负责 rawfile 同步、URI 文件复制和沙箱文件读写;
  • napi_init.cpp:通过 N-API 暴露 runRequestJson(),并启动 HNP Python;
  • fonttools_hnp_bridge.py:真正 import fontTools 并执行字体任务的 Python worker。

这里没有改写 Lib/fontTools 上游源码,而是在外面增加了一层鸿蒙 PC 调用壳。这样做的好处是边界清晰:fontTools 保持原有 Python 库形态,鸿蒙适配逻辑集中在 examples/harmony_pc/

在这里插入图片描述

五、ArkTS 页面:从测试 demo 改成真实用户流程

最开始技术验证时,很容易写成“内置样例文件”“验证按钮”“固定路径输出”这种 demo。它能证明链路能跑,但用户使用时并不自然。

这次后续把页面改成了真实用户流程:

  1. 用户点击 选择字体
  2. 通过 DocumentViewPicker.select() 从鸿蒙 PC 文件选择器选择 .ttf.otf.ttc.otc.woff.woff2
  3. 应用把选择器返回的 URI 复制到应用沙箱;
  4. 用户可以点击 查看字体信息
  5. 用户输入要保留的文字,点击 裁剪并导出小字体
  6. 通过 DocumentViewPicker.save() 选择最终保存位置;
  7. Python bridge 调用 fontTools 生成输出文件;
  8. ArkTS 再把沙箱输出复制到用户选择的保存 URI。

之所以要多一步“复制到应用沙箱”,是因为鸿蒙文件选择器返回的是 URI,而 Python 三方库通常更习惯处理真实文件路径。直接把 URI 交给 Python,不一定稳定。这里采用的是:

用户选择的字体 URI
  -> RawFileSync.copyFile()
  -> 应用沙箱 input 路径
  -> Python 读取 input
  -> Python 写入 output
  -> RawFileSync.copyFile()
  -> 用户选择的保存 URI

页面没有把应用沙箱路径展示给用户。用户看到的是“选择字体”“保存位置”“导出成功”等真实操作结果,而不是一堆内部路径。

界面也拆成了四个独立模块:

  • 模块一:运行环境检查;
  • 模块二:选择原字体;
  • 模块三:按文本裁剪小字体;
  • 模块四:TTX 转换。

这样普通用户可以直接理解“选择原字体”和“裁剪小字体”,开发者也可以使用 TTX 导出和编译能力。

在这里插入图片描述

六、ArkTS Client:把字体操作变成 JSON 请求

FontToolsHnpClient.ets 是应用侧调用 Python 的封装层。它不直接处理字体,而是负责组织请求、准备目录、调用 Native,并解析 Python 返回值。

它维护了几个应用沙箱路径:

fonttools-hnp/
  inputs/
  outputs/
  python/
  fonttools_hnp_bridge.py

每次调用前,prepare() 会确保目录存在,并把 rawfile 中的 Python bridge 同步到沙箱:

prepare(): void {
  RawFileSync.ensureDir(this.paths.workDir);
  RawFileSync.ensureDir(this.paths.inputDir);
  RawFileSync.ensureDir(this.paths.outputDir);
  RawFileSync.syncDirectory(this.context.resourceManager, 'python', this.paths.pythonDir);
}

比如查看字体信息时,会生成类似这样的 JSON 请求:

{
  "op": "info",
  "input": "/data/storage/el2/base/files/fonttools-hnp/inputs/selected_font.ttf",
  "font_number": 0
}

按文本裁剪小字体时,会生成:

{
  "op": "subset",
  "input": "/data/storage/el2/base/files/fonttools-hnp/inputs/selected_font.ttf",
  "output": "/data/storage/el2/base/files/fonttools-hnp/outputs/selected_font_subset.ttf",
  "text": "HarmonyOS 字体"
}

然后统一调用 Native:

fontToolsHnpNative.runRequestJson(
  this.pythonPath,
  this.paths.bridgeScriptPath,
  requestPath,
  this.paths.workDir
);

这样 ArkTS 页面不需要知道 Python 命令怎么拼,也不需要直接操作 fontTools。页面只需要调用 version()info()dumpTtx()compileTtx()subset() 这些方法即可。

七、Native/N-API:启动 HNP Python 进程

ArkTS 本身不能直接 import fontTools,所以中间需要 Native/N-API 做桥接。

napi_init.cpp 暴露了两个方法:

getDefaultPythonPath()
runRequestJson(pythonPath, bridgeScriptPath, requestJsonPath, workingDir)

其中 runRequestJson() 会拼出 HNP Python 启动命令。为了减少 Python 运行时的缓存和线程问题,命令里设置了几个环境变量:

PYTHONDONTWRITEBYTECODE=1
PYTHONUTF8=1
PYTHONIOENCODING=utf-8
OPENBLAS_NUM_THREADS=1
OMP_NUM_THREADS=1
GOTO_NUM_THREADS=1
NUMEXPR_NUM_THREADS=1
VECLIB_MAXIMUM_THREADS=1
OPENBLAS_MAIN_FREE=1

虽然 fontTools 本身不像 NumPy 那样大量使用计算线程,但这套环境变量对 Python 三方库适配比较通用,可以避免某些依赖在移动/PC 沙箱环境里开过多线程。

Native 层最终执行的逻辑可以理解成:

cd <workingDir>
<pythonPath> <bridgeScriptPath> --request <requestJsonPath> 2>&1

Python 的 stdout 会被 Native 捕获,再返回给 ArkTS。成功时返回 JSON,例如:

{
  "ok": true,
  "op": "subset",
  "output_size": 12345,
  "before_glyph_count": 3000,
  "after_glyph_count": 12
}

失败时也返回 JSON,包含错误类型、错误信息和 traceback,方便页面展示和排查。

八、Python bridge:真正调用 fontTools

真正处理字体的是 entry/src/main/resources/rawfile/python/fonttools_hnp_bridge.py

它支持五个操作:

version
info
dump_ttx
compile_ttx
subset

version 用来检查环境:

def op_version(request):
    return {
        "ok": True,
        "op": "version",
        "python": sys.version,
        "python_executable": sys.executable,
        "fonttools": FONTTOOLS_VERSION,
    }

info 会打开字体并汇总常用信息:

  • 字体名称;
  • 字重样式;
  • 字体表列表;
  • glyph 数量;
  • 文件大小;
  • UPM;
  • bbox;
  • ascent / descent;
  • OS/2 weight class。

dump_ttx 使用 font.saveXML() 把字体导出成 TTX XML:

font.saveXML(
    str(output_path),
    tables=tables,
    skipTables=skip_tables,
    splitTables=bool(request.get("split_tables", False)),
    disassembleInstructions=bool(request.get("disassemble_instructions", True)),
)

compile_ttx 使用 font.importXML() 把 TTX 编译回字体:

font = TTFont(recalcTimestamp=False)
font.importXML(str(input_path))
font.save(str(output_path))

subset 使用 fontTools.subset.Subsetter 按文本、glyph 或 Unicode 码位裁剪字体:

options = Options()
subsetter = Subsetter(options=options)
subsetter.populate(text=text)
subsetter.subset(font)
font.save(str(output_path))

这样用户输入“鸿蒙字体”时,并不是把这几个字写进字体文件,而是从原字体里找到这些字对应的 glyph,只保留这些 glyph 以及必要的字体表,最后导出一个新的小字体。

在这里插入图片描述

九、TTX 转换:开发者更容易看懂字体结构

TTX 可以理解成字体文件的 XML 展开形式。普通用户不一定会用到它,但对字体工程、开发调试很有用。

在页面里,TTX 转换分成两个独立操作:

  • 导出为 TTX 文件:从已选择的字体导出 TTX XML;
  • 选择 TTX + 编译回字体:选择一个 TTX XML,再生成字体文件。

这里的关系可以这样理解:

原字体 .ttf/.otf/.ttc
  -> 导出为 TTX
  -> 得到 .ttx XML 文件
  -> 修改或查看 XML
  -> 编译回字体
  -> 得到新的 .ttf 文件

这几个模块是相互独立的。用户只想生成小字体,可以只用“选择原字体”和“按文本裁剪小字体”。开发者想看字体内部结构,可以使用 TTX 导出。需要把 TTX 重新变成字体时,再使用“选择 TTX”和“编译回字体”。

十、构建验证和问题修复

工程可以使用 DevEco Studio 直接构建,也可以使用 hvigor 命令构建:

/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
  --mode module \
  -p module=entry@default \
  -p product=default \
  -p requiredDeviceType=2in1 \
  assembleHap \
  --analyze=normal \
  --parallel \
  --incremental \
  --daemon

适配过程中遇到过一个典型 UI 问题:页面模块变多后,鸿蒙 PC 窗口最大化时不能上下滚动,底部内容看不到。后来把整页包进外层 Scroll(),并明确设置纵向滚动:

Scroll() {
  Column({ space: 16 }) {
    ...
  }
}
.width('100%')
.height('100%')
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)

这里还遇到过一个编译细节:当前 ArkTS 版本里 TextAttribute 不支持 .minHeight(),如果把 .minHeight(120) 写在 Text 上,会出现:

Property 'minHeight' does not exist on type 'TextAttribute'

最终处理方式是去掉 Text 上不兼容的 .minHeight(),保留外层滚动能力。重新构建后,CompileArkTSPackageHapSignHap 都可以通过。

构建成功后可以看到类似输出:

Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
Finished :entry:default@SignHap
Finished :entry:assembleHap
BUILD SUCCESSFUL

在这里插入图片描述

十一、这次适配后的结果

完成后,这个示例已经不是一个简单的验证按钮,而是一个更接近真实用户使用场景的鸿蒙 PC 三方库工具:

  • 用户从文件选择器选择真实字体;
  • 页面不展示内部沙箱路径;
  • 导出时由用户通过保存选择器选择最终位置;
  • 功能按模块拆分,普通用户和开发者都能理解;
  • HNP Python 和 fontTools 的调用链清晰;
  • Native 层只负责启动 Python,不耦合字体业务;
  • Python bridge 支持版本检查、字体信息、TTX 导出、TTX 编译、子集字体;
  • 页面可以在鸿蒙 PC 最大化窗口下正常上下滚动;
  • DevEco/Hvigor 构建可以通过。

从这次适配可以总结出一套比较通用的 Python 三方库鸿蒙 PC 接入思路:

先在 HNP Python 中验证三方库
  -> 再写 Python worker 承接 JSON 请求
  -> 用 N-API 启动 Python 并捕获 stdout
  -> ArkTS Client 封装业务方法
  -> ArkUI 页面走真实文件选择和保存流程
  -> 最后用真机和 DevEco 构建验证闭环

fontTools 这样的 Python 库来说,这条路线比重写底层字体能力更稳,也更容易推广到其他 Python 工具库。后续如果要继续扩展,可以在这个基础上增加更多字体工程能力,例如指定字体集合索引、保留 OpenType features、输出 WOFF/WOFF2、批量子集化、多字体对比等。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐