欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: unisources/TypeFun — 鸿蒙PC文字特效编辑器
集成平台: 鸿蒙PC| 测试SDK: 6.1.1(24) | DevEco Studio 6.1


image-20260618103757366

一、前置说明

项目 说明
集成库 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 个阶段,且每个阶段都可能因为工具链差异导致返工。

链接失败

运行时崩溃

缺失传递依赖

工程搭建

库文件部署

CMake配置

NAPI桥接

类型声明

UI验证

编译测试

排查.so依赖链

阶段 主要痛点
工程搭建 配置 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.alibpng.alibfreetype.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_typesdefault_langs),使用绝对寻址。共享库 libentry.so 要求所有代码为 PIC(位置无关代码),非 PIC 的 .o 无法链接到 .so

修复:既然我们的代码不调用 fontconfig API,直接移除 .a 链接,仅保留头文件路径供编译:

- ${fontconfig_ROOT}/lib/libfontconfig.a

坑 3:运行时 .so 传递依赖缺失

现象:App 启动时 testNapiundefined,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.soliblzma.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 或其他渲染库时遇到过什么坑?欢迎在评论区分享。

如果本文对你有帮助,请 点赞、收藏、转发 支持一下~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐