鸿蒙PC集成Cairo渲染引擎:踩了5个坑才跑通NAPI管线(附代码)
欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: unisources/TypeFun — 鸿蒙PC文字特效编辑器
集成平台: 鸿蒙PC| 测试SDK: 6.1.1(24) | DevEco Studio 6.1

一、前置说明
| 项目 | 说明 |
|---|---|
| 集成库 | Cairo 1.17.8 (.so) / fontconfig 2.14.2 (.a) / libpng 1.6.39 (.a) |
| 目标平台 | 鸿蒙PC (OpenHarmony arm64-v8a) |
| SDK 版本 | HarmonyOS SDK 6.1.1(24) — API 12 |
| 开发工具 | DevEco Studio 6.1 |
| 交叉编译工具链 | lycium_plusplus (BiSheng 编译器) |
| 前置依赖 | zlib 1.2.13 / bzip2 / brotli / libexpat 1.8.10 / pixman 0.42.2 (周期1) + freetype2 25.0.19 / harfbuzz 6.7.10 (周期2) |
| 项目地址 | https://atomgit.com/unisources/TypeFun |
依赖链总览
周期3在整个项目中的位置:
周期1: zlib + bzip2 + brotli + libexpat + pixman (编译基础设施)
周期2: freetype2 + harfbuzz (字体引擎)
周期3: fontconfig + Cairo + libpng ← 本次 ← 渲染引擎
│ │ └─ zlib (字体缓存压缩)
│ └─ pixman + freetype2 + fontconfig + libpng
└─ libexpat + freetype2
Cairo 的运行时 .so 依赖链:
libentry.so
└─ libcairo.so
├─ libz.so.1 (压缩)
├─ libpng16.so.16 (PNG编解码)
├─ libfontconfig.so.1 (字体配置)
│ ├─ libxml2.so (XML解析)
│ │ └─ liblzma.so.5 (XZ压缩)
│ └─ libz.so
├─ libpixman-1.so.0 (像素操作)
└─ libexpat.so.1 (XML)
二、传统方式的效率瓶颈
在没有自动化工具辅助的情况下,从零集成 Cairo 渲染引擎需要经过 7 个阶段,且每个阶段都可能因为工具链差异导致返工。
| 阶段 | 主要痛点 |
|---|---|
| 工程搭建 | 配置 abiFilters、module.json5 权限 |
| 库文件部署 | 10 个库的 .a/.so 复制到正确架构目录 |
| CMake 配置 | 10 个库的链接顺序、include_directories 管理 |
| NAPI 桥接 | Cairo 的 cairo_t/cairo_surface_t 生命周期管理 |
| 类型声明 | .d.ts 接口签名与 C++ 精确匹配 |
| 运行时 .so | 传递依赖链排查困难(缺一个 .so 整个 app 无法启动) |
三、AtomCode + Skills 全流程
3.1 CMakeLists.txt 配置
周期3新增 fontconfig、cairo、libpng 三个库的路径和链接:
# 路径宏(统一管理所有库)
macro(setup_lib LIB_NAME)
set(${LIB_NAME}_ROOT ${THIRDPARTY_ROOT}/${LIB_NAME}/${OHOS_ARCH})
endmacro()
setup_lib(fontconfig)
setup_lib(cairo)
setup_lib(libpng)
# 头文件路径
include_directories(
${fontconfig_ROOT}/include # 被 cairo-ft.h 条件包含
${cairo_ROOT}/include/cairo
${libpng_ROOT}/include
)
# 链接(注意 libpng16 放在 fontconfig 之前)
target_link_libraries(entry PUBLIC
${libpng_ROOT}/lib/libpng16.a
${fontconfig_ROOT}/lib/libfontconfig.a
${cairo_ROOT}/lib/libcairo.so
)
要点:
libpng16.a必须置于libfontconfig.a之前,因为 fontconfig 内部引用了 PNG 符号,静态库链接顺序是依赖者在后。
3.2 canvas_bridge.cpp — Cairo NAPI 桥接
核心是 canvas 命名空间,提供 create → drawGlyphs → exportPNG 完整管线:
// 画布缓存
struct CanvasEntry {
cairo_surface_t *surface;
cairo_t *cr;
int width, height;
};
static std::vector<CanvasEntry> g_canvases;
// canvas.create(width, height) → canvasId
static napi_value CanvasCreate(napi_env env, napi_callback_info info) {
cairo_surface_t *surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32, width, height);
cairo_t *cr = cairo_create(surface);
// 白色背景
cairo_set_source_rgb(cr, 1, 1, 1);
cairo_paint(cr);
g_canvases.push_back({surface, cr, width, height});
return canvasId;
}
字形绘制采用了 Cairo 的 cairo_show_glyphs 批量渲染,结合 HarfBuzz 排版结果:
// canvas.drawGlyphs(canvasId, glyphs, params)
// params: { fontId, fontSize, color, x, y }
static napi_value CanvasDrawGlyphs(napi_env env, napi_callback_info info) {
// 解析 fontId → 获取 FT_Face
cairo_font_face_t *cairoFace =
cairo_ft_font_face_create_for_ft_face(face, 0); // 0=不合成加粗/倾斜
cairo_set_font_face(canvas.cr, cairoFace);
cairo_set_font_size(canvas.cr, fontSize);
// 解析颜色 #RRGGBB → rgba
cairo_set_source_rgba(canvas.cr, r, g, b, a);
// 构建 cairo_glyph_t 数组
std::vector<cairo_glyph_t> cairoGlyphs;
double cursorX = params.x, cursorY = params.y;
for (auto &g : glyphs) {
cairoGlyphs.push_back({
.index = (unsigned long)g.glyphId,
.x = cursorX + g.xOffset,
.y = cursorY + g.yOffset,
});
cursorX += g.xAdvance;
}
cairo_show_glyphs(canvas.cr, cairoGlyphs.data(),
(int)cairoGlyphs.size());
cairo_font_face_destroy(cairoFace);
}
PNG 导出仅需一行 Cairo API:
// canvas.exportPNG(canvasId, path)
cairo_status_t status = cairo_surface_write_to_png(surface, filePath);
3.3 命名空间注册
在 napi_init.cpp 中注册 canvas 命名空间:
napi_value canvasNs = CreateCanvasNamespace(env);
napi_set_named_property(env, exports, "canvas", canvasNs);
3.4 ArkTS 类型声明
export interface DrawParams {
fontId: number;
fontSize: number;
color: string; // "#RRGGBB"
x?: number;
y?: number;
}
export const canvas: {
create(width: number, height: number): number;
drawGlyphs(canvasId: number, glyphs: GlyphPos[], params: DrawParams): void;
exportPNG(canvasId: number, path: string): void;
releaseCanvas(canvasId: number): void;
};
3.5 编辑器 UI
ArkTS 编辑器页面提供完整交互:
// 实时预览:任意参数变化触发自动渲染
TextInput({ placeholder: '输入文字...', text: this.text })
.onChange((v) => { this.text = v; this.autoRender(); })
Slider({ value: this.fontSize, min: 16, max: 128, step: 2, style: SliderStyle.OutSet })
.onChange((v) => { this.fontSize = v; this.autoRender(); })
// 12色色板
ForEach(COLOR_PALETTE, (color: string) => {
GridItem() {
Circle().fill(color)
.stroke(this.selectedColor === color ? accentColor : Transparent)
}.onClick(() => { this.selectedColor = color; this.autoRender(); })
})
// 深色主题切换
Text(this.isDark ? '☀️' : '🌙')
.onClick(() => { this.isDark = !this.isDark; })
四、踩坑专区
坑 1:fontconfig .a 嵌套归档
现象:
libfontconfig.a: archive member 'libbz2.a' is neither ET_REL nor LLVM bitcode
根因:Fontconfig 使用 autotools/libtool 构建时,-all-static 标志把依赖的 libbz2.a、libpng.a、libfreetype.a 作为归档成员直接嵌入到 libfontconfig.a 中。BiSheng 链接器要求归档内成员是 .o 文件或 LLVM bitcode,不识别嵌套的 .a。
修复:提取所有 .o 重新打包:
cd lib/
# 提取 fontconfig 自己的 .o
ar x libfontconfig.a $(ar t libfontconfig.a | grep '\.o$')
# 删除原文件,重建纯 .o 归档
rm -f libfontconfig.a
ar rcs libfontconfig.a *.o
但更好的方案是——不链接 fontconfig.a。我们的代码不直接调用 fontconfig API,Cairo 运行时通过共享库 libfontconfig.so 解析自己的 fontconfig 符号。
坑 2:非 PIC 代码链接共享库
现象:
ld.lld: error: relocation R_AARCH64_ADR_PREL_PG_HI21
cannot be used against symbol 'other_types'; recompile with -fPIC
根因:fcobjs.o / fcdefault.o 等 fontconfig 目标文件引用了全局变量(other_types、default_langs),使用绝对寻址。共享库 libentry.so 要求所有代码为 PIC(位置无关代码),非 PIC 的 .o 无法链接到 .so。
修复:既然我们的代码不调用 fontconfig API,直接移除 .a 链接,仅保留头文件路径供编译:
- ${fontconfig_ROOT}/lib/libfontconfig.a
坑 3:运行时 .so 传递依赖缺失
现象:App 启动时 testNapi 为 undefined,NAPI 模块加载失败。检查 log:
Failed to open libhint2type library ... No such file or directory
根因:libcairo.so 的 NEEDED 链为:
libcairo.so → libfontconfig.so → libxml2.so → liblzma.so.5
→ libz.so
其中 libxml2.so 和 liblzma.so.5 未打包到 entry/libs/arm64-v8a/,导致动态链接器无法加载整个链。
修复:从 lycium 构建产物中复制缺失的 .so:
cp -a lycium/usr/libxml2/arm64-v8a/lib/libxml2.so entry/libs/arm64-v8a/
cp -a lycium/usr/xz/arm64-v8a/lib/liblzma.so* entry/libs/arm64-v8a/
坑 4:ArkTS Strict Mode 对象字面量类型
现象:
ArkTS Compiler Error: Object literals cannot be used as type declarations
根因:API 12 的 ArkTS strict mode 禁止在类型注解中使用内联对象字面量。
// ❌ 禁止:内联对象字面量作为类型声明
@State fontOptions: { id: number; name: string }[] = [];
// ✅ 正确:提取为独立接口
interface FontOption { id: number; name: string; }
@State fontOptions: FontOption[] = [];
同类问题还包括 build() 内的非组件语句:
build() {
const bgColor = ...; // ❌ 非 UI 语法
Column() { ... }
}
// ✅ 正确:移到 getter 属性
get bgColor(): string { return this.isDark ? '#1a1a2e' : '#f5f5f5'; }
坑 5:页面路由字体状态不同步
现象:Index 页下载字体后,通过 router.pushUrl 跳转到 EditorPage,编辑器提示"请下载字体"。
根因:Index 页通过 font.loadFont() 将字体存入 C++ 全局 g_fontFaces,但 EditorPage 的 getFontList() 返回空数组。router.pushUrl 可能导致 NAPI 模块状态被隔离。
修复:通过路由参数传递 fontId:
// Index 页
const list = testNapi.font.getFontList();
const lastId = list.length > 0 ? list[list.length - 1].id : 0;
router.pushUrl({
url: 'pages/EditorPage',
params: { fontId: lastId }
});
// EditorPage
aboutToAppear() {
const params = router.getParams();
const fontId = params['fontId'];
this.initFont(fontId);
}
五、运行时依赖清单
entry/libs/arm64-v8a/
├── libcairo.so ← Cairo 渲染引擎 (核心)
├── libfontconfig.so ← 字体配置
├── libxml2.so ← XML 解析 (fontconfig 依赖)
├── liblzma.so ← XZ 压缩 (libxml2 依赖)
├── libpng16.so ← PNG 编码 (cairo 依赖)
├── libpixman-1.so ← 像素操作 (cairo 依赖)
├── libz.so ← 数据压缩 (多处依赖)
└── libexpat.so ← XML (fontconfig 编译依赖)
共 8 个 .so,构成完整的运行时依赖树。缺少任意一个,
libentry.so加载失败,app 无法启动。
六、通用集成模板(拿来即用)
运行时 .so 依赖排查脚本
#!/bin/bash
# 检查所有 .so 的 NEEDED 依赖树
for f in entry/libs/arm64-v8a/*.so*; do
[ -f "$f" ] && [ ! -L "$f" ] && {
deps=$(readelf -d "$f" 2>/dev/null | grep NEEDED)
echo "$(basename $f):"
echo "$deps" | sed 's/^/ /'
}
done
NAPI 命名空间工厂模板
napi_value CreateMyNamespace(napi_env env) {
napi_value ns;
napi_create_object(env, &ns);
napi_property_descriptor desc[] = {
{"func1", nullptr, Func1Impl, nullptr, nullptr, nullptr, napi_default, nullptr},
{"func2", nullptr, Func2Impl, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, ns, sizeof(desc)/sizeof(desc[0]), desc);
return ns;
}
七、项目文件清单(周期3核心)
entry/src/main/cpp/
├── canvas_bridge.h # canvas 命名空间声明
├── canvas_bridge.cpp # Cairo NAPI 桥接 (336行)
└── napi_init.cpp # 注册 canvas 命名空间
entry/src/main/cpp/types/libentry/
└── Index.d.ts # DrawParams + canvas 类型
entry/src/main/ets/pages/
├── EditorPage.ets # 编辑器 UI (288行)
└── Index.ets # 首页 + 导航
entry/libs/arm64-v8a/ # 运行时 8 个 .so
├── libcairo.so
├── libfontconfig.so
├── libxml2.so
├── liblzma.so
├── libpng16.so
├── libpixman-1.so
├── libz.so
└── libexpat.so
八、总结
周期3在 3 个渲染库上踩了 5 个坑,其中最典型的是 fontconfig 的 .a 嵌套归档、非 PIC 代码链接共享库 以及 运行时 .so 传递依赖缺失。这些坑的共同特征是:编译通过 ≠ 运行通过,链接器不报错不代表动态加载器能工作。
回顾整个周期3的开发,有 3 个经验值得记住:
第一,静态库的链接陷阱。fontconfig 的 libtool 在 -all-static 模式下会把依赖库嵌入自身,而 OHOS 的 BiSheng 链接器不接受归档中的归档。解决方法是优先使用共享库(.so)而非静态库(.a),或者在构建三方库时关闭 libtool 的合并行为。
第二,运行时依赖的可见性。Cairo 是共享库,它的 NEEDED 声明了完整的传递依赖链。readelf -d libcairo.so | grep NEEDED 能直接看到它需要哪些 .so。将每个 NEEDED 递归展开,就能得到完整的运行时依赖树。这是一个在集成初期就该执行的检查——不要等到 app 启动失败再排查。
第三,ArkTS strict mode 的适配成本。API 12 的 ArkTS 编译器比标准 TypeScript 严格得多:内联对象类型、build() 中的非组件语句、对象字面量类型断言都会报错。建议在写 ArkTS 代码时遵循"接口先定义、逻辑放 getter、build 只放 UI"的原则。
Cairo 集成教会我们的道理:鸿蒙 NAPI 集成的核心不是写 C++ 代码,而是管好链接顺序、PIC 标志和运行时 .so 依赖树。
下一步周期4将集成 cppjieba(中文分词)和 opencc(简繁转换),实现文案工坊功能。
你在集成 Cairo 或其他渲染库时遇到过什么坑?欢迎在评论区分享。
如果本文对你有帮助,请 点赞、收藏、转发 支持一下~
更多推荐



所有评论(0)