Flutter PDF 渲染插件(pdf_image_renderer)适配鸿蒙 (HarmonyOS) 平台实战
本文详细介绍了将 Flutter 插件 pdf_image_renderer 适配到鸿蒙平台的技术方案。由于鸿蒙缺乏原生 PDF 渲染 API,作者选择基于 pdfium 库通过 NAPI 实现高性能渲染。文章重点阐述了整体架构设计(包含 Flutter、ArkTS 和 C++ NAPI 三层),以及核心实现细节,如 NAPI 模块注册、PDF 渲染逻辑和跨平台兼容处理。该方案成功解决了鸿蒙平台高

本文详细记录了将
pdf_image_rendererFlutter 插件从 Android/iOS 适配到鸿蒙 (HarmonyOS/OpenHarmony) 平台的完整过程,包括技术方案选型、NAPI 原生模块开发、pdfium 库集成等核心内容。
一、项目背景
1.1 pdf_image_renderer 插件简介
pdf_image_renderer 是一个 Flutter 插件,用于将 PDF 文件渲染为位图图片。它使用各平台的原生渲染器来实现高质量的 PDF 渲染:
- Android: 使用
android.graphics.pdf.PdfRenderer - iOS: 使用
CGPDFDocument/CGPDFPage - HarmonyOS: 使用 pdfium 库通过 NAPI 实现(本文重点)
1.2 为什么需要适配鸿蒙?
随着 HarmonyOS NEXT 的发布,越来越多的开发者需要将现有的 Flutter 应用适配到鸿蒙平台。然而,鸿蒙系统没有内置的 PDF 渲染 API(不像 Android 的 PdfRenderer 或 iOS 的 CGPDFDocument),这给 PDF 相关功能的适配带来了挑战。
二、技术方案设计
2.1 方案调研
经过调研,鸿蒙平台渲染 PDF 的可选方案有:
| 方案 | 优点 | 缺点 |
|---|---|---|
| WebView 渲染 | 实现简单 | 性能差,无法获取位图数据 |
| 纯 Dart PDF 库 | 跨平台 | 渲染质量一般,性能受限 |
| pdfium + NAPI | 高性能,渲染质量好 | 需要 C++ 开发,复杂度较高 |
最终选择 pdfium + NAPI 方案,因为它能提供与 Android/iOS 原生渲染器同等质量的 PDF 渲染效果。
2.2 整体架构
┌─────────────────────────────────────────────────────────┐
│ Flutter (Dart) │
│ PdfImageRenderer API │
└─────────────────────┬───────────────────────────────────┘
│ MethodChannel
▼
┌─────────────────────────────────────────────────────────┐
│ ArkTS Plugin Layer │
│ PdfImageRendererPlugin.ets │
│ - 处理 MethodChannel 调用 │
│ - 调用 NAPI 函数 │
│ - PNG 图片编码 (@ohos.multimedia.image) │
└─────────────────────┬───────────────────────────────────┘
│ NAPI Import
▼
┌─────────────────────────────────────────────────────────┐
│ C++ NAPI Layer │
│ libpdf_renderer.so │
│ - PDF 文档管理 │
│ - 页面渲染 │
│ - BGRA → RGBA 像素转换 │
└─────────────────────┬───────────────────────────────────┘
│ 动态链接
▼
┌─────────────────────────────────────────────────────────┐
│ libpdfium.so │
│ (预编译 PDF 渲染引擎) │
└─────────────────────────────────────────────────────────┘
三、实现详解
3.1 目录结构
ohos/
├── oh-package.json5 # 包配置
├── build-profile.json5 # 构建配置(含 CMake)
├── Index.ets # 入口文件
├── src/main/
│ ├── module.json5 # 模块配置
│ ├── ets/components/plugin/
│ │ └── PdfImageRendererPlugin.ets # ArkTS 插件主类
│ ├── cpp/
│ │ ├── CMakeLists.txt # C++ 构建配置
│ │ ├── napi_init.cpp # NAPI 模块注册
│ │ ├── pdf_renderer_napi.cpp # PDF 渲染实现
│ │ ├── pdf_renderer_napi.h
│ │ ├── include/
│ │ │ └── fpdfview.h # pdfium 头文件
│ │ └── types/libpdf_renderer/
│ │ ├── index.d.ts # TypeScript 类型声明
│ │ └── oh-package.json5
│ └── libs/arm64-v8a/
│ └── libpdfium.so # 预编译 pdfium 库
3.2 NAPI 模块实现
3.2.1 模块注册 (napi_init.cpp)
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"openPdf", nullptr, NapiOpenPdf, nullptr, nullptr, nullptr, napi_default, nullptr},
{"closePdf", nullptr, NapiClosePdf, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getPageCount", nullptr, NapiGetPageCount, nullptr, nullptr, nullptr, napi_default, nullptr},
{"openPage", nullptr, NapiOpenPage, nullptr, nullptr, nullptr, napi_default, nullptr},
{"closePage", nullptr, NapiClosePage, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getPageWidth", nullptr, NapiGetPageWidth, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getPageHeight", nullptr, NapiGetPageHeight, nullptr, nullptr, nullptr, napi_default, nullptr},
{"renderPage", nullptr, NapiRenderPage, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
// 初始化 pdfium 库
PdfRendererNapi::GetInstance().InitLibrary();
return exports;
}
3.2.2 PDF 渲染核心逻辑
uint8_t* PdfRendererNapi::RenderPage(int docId, int pageIndex,
int x, int y, int width, int height,
double scale, uint32_t backgroundColor,
int* outDataSize) {
// 1. 创建位图
FPDF_BITMAP bitmap = FPDFBitmap_Create(scaledWidth, scaledHeight, 1);
// 2. 填充背景色
FPDFBitmap_FillRect(bitmap, 0, 0, scaledWidth, scaledHeight, backgroundColor);
// 3. 渲染页面到位图
FPDF_RenderPageBitmap(bitmap, page, renderX, renderY,
renderWidth, renderHeight, 0, FPDF_ANNOT | FPDF_LCD_TEXT);
// 4. BGRA → RGBA 转换(pdfium 输出 BGRA,但 HarmonyOS image API 需要 RGBA)
uint8_t* src = (uint8_t*)FPDFBitmap_GetBuffer(bitmap);
for (int i = 0; i < scaledWidth * scaledHeight; i++) {
int offset = i * 4;
outputData[offset + 0] = src[offset + 2]; // R <- B
outputData[offset + 1] = src[offset + 1]; // G <- G
outputData[offset + 2] = src[offset + 0]; // B <- R
outputData[offset + 3] = src[offset + 3]; // A <- A
}
return outputData;
}
3.3 ArkTS 插件层
import { picker } from '@kit.CoreFileKit';
import { fileUri } from '@kit.CoreFileKit';
import image from '@ohos.multimedia.image';
import pdfRenderer from 'libpdf_renderer.so';
export default class PdfImageRendererPlugin implements FlutterPlugin, MethodCallHandler {
// 文件选择器
private async handlePickPdfFile(call: MethodCall, result: MethodResult): Promise<void> {
const documentSelectOptions = new picker.DocumentSelectOptions();
documentSelectOptions.fileSuffixFilters = ['.pdf', '.PDF'];
const documentPicker = new picker.DocumentViewPicker();
const uris = await documentPicker.select(documentSelectOptions);
if (uris && uris.length > 0) {
const path = new fileUri.FileUri(uris[0]).path;
result.success(path);
}
}
// 渲染 PDF 页面
private async handleRenderPDFPage(call: MethodCall, result: MethodResult): Promise<void> {
// 1. 调用 NAPI 渲染获取 RGBA 像素数据
const renderResult = pdfRenderer.renderPage(docId, pageIndex, x, y, width, height, scale, bgColor);
// 2. 创建 PixelMap
const pixelMap = await image.createPixelMap(renderResult.data.buffer, {
size: { width: renderResult.width, height: renderResult.height },
pixelFormat: image.PixelMapFormat.RGBA_8888,
});
// 3. 编码为 PNG
const packer = image.createImagePacker();
const pngBuffer = await packer.packing(pixelMap, { format: 'image/png', quality: 100 });
result.success(new Uint8Array(pngBuffer));
}
}
3.4 CMake 配置(关键!)
预编译库的正确引入方式:
# 错误方式(库不会被打包到 HAP)
link_directories(${PDFIUM_LIB_PATH})
target_link_libraries(pdf_renderer pdfium)
# 正确方式(库会被自动打包到 HAP)
add_library(pdfium SHARED IMPORTED)
set_target_properties(pdfium PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../libs/${OHOS_ARCH}/libpdfium.so)
target_link_libraries(pdf_renderer PUBLIC pdfium)
四、遇到的问题与解决方案
4.1 pdfium 库获取
问题:鸿蒙平台没有预编译的 pdfium 库。
解决:从 bblanchon/pdfium-binaries 下载 linux-musl-arm64 版本,该版本与 HarmonyOS 的 musl libc 兼容。
curl -L -o pdfium.tgz https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-arm64.tgz
4.2 NAPI 模块加载失败
问题:运行时报错 Cannot read property openPdf of undefined。
原因:libpdfium.so 没有被打包到 HAP 中。
解决:使用 CMake 的 IMPORTED 库方式引入预编译库,确保其被正确打包。
4.3 file_picker 不支持 OHOS
问题:Flutter 的 file_picker 包不支持 OHOS 平台。
解决:在插件中实现原生文件选择器,使用 @kit.CoreFileKit 的 picker API。
五、使用方法
5.1 安装
dependencies:
pdf_image_renderer: ^1.0.1
5.2 基本使用
// 1. 创建渲染器
final pdf = PdfImageRenderer(path: '/path/to/document.pdf');
// 2. 打开 PDF
await pdf.open(password: 'optional_password');
// 3. 获取页数
final pageCount = await pdf.getPageCount();
// 4. 渲染页面为图片
final imageData = await pdf.renderPage(
pageIndex: 0,
scale: 2.0, // 2倍分辨率
background: Colors.white,
);
// 5. 显示图片
Image(image: MemoryImage(imageData!))
// 6. 关闭 PDF
await pdf.close();
5.3 OHOS 平台文件选择
// 使用原生文件选择器(仅 OHOS)
final path = await PdfImageRendererPlatform.instance.pickPdfFile();
if (path != null) {
final pdf = PdfImageRenderer(path: path);
await pdf.open();
// ...
}
六、性能数据
| 测试项 | 设备 | 结果 |
|---|---|---|
| 打开 10 页 PDF | HarmonyOS NEXT | ~50ms |
| 渲染单页 (A4, scale=1) | HarmonyOS NEXT | ~30ms |
| 渲染单页 (A4, scale=3) | HarmonyOS NEXT | ~150ms |
| 内存占用 | - | 约 20MB (含 pdfium) |
七、总结
本次适配工作的主要成果:
- 完整实现 PDF 渲染功能,包括打开/关闭文档、获取页数、获取尺寸、渲染页面
- 支持密码保护 的 PDF 文件
- 支持自定义渲染参数:缩放比例、裁剪区域、背景颜色
- 原生文件选择器 集成
- 高性能渲染:使用 pdfium 引擎,渲染质量与 Chrome 浏览器一致
关键技术点
- OHOS NAPI (Node-API) 开发
- CMake 预编译库集成
- ArkTS 与 C++ 混合编程
- Flutter MethodChannel 通信
- HarmonyOS 多媒体 API (image 模块)
源码仓库
- 插件地址:pdf_image_renderer
更多推荐



所有评论(0)