PDF Kit 使用示例(HarmonyOS)
前言
说起PDF,开发时总绕不开。最早做PDF相关功能,是帮同事搞个合同预览,结果一头雾水,踩了不少坑。后来用多了,发现HarmonyOS的PDF Kit其实挺顺手,能编辑、能预览、还能加批注,基本上开发需求都能覆盖。
这篇笔记就当是给后来人留个"避坑指南",也顺便记录下自己踩过的那些小坑和收获的经验。希望你用PDF Kit时,能少走点弯路,多点乐趣。
简介
PDF Kit(PDF服务)为HarmonyOS应用提供了丰富的PDF文档处理能力,包含 pdfService
和 PdfView
两大核心模块。
- pdfService:支持加载、保存、编辑PDF文档,包括添加文本、图片、批注、页眉页脚、水印、背景、书签、加解密等。
- PdfView:提供PDF文档预览、页面跳转、缩放、关键字搜索、高亮、批注等功能。
有时候,产品一句"能不能加个PDF批注",开发就得从头到尾撸一遍API。别慌,下面这些例子和故事,都是我踩过的"真实路"。
更多示例可参考官方CodeLab和SampleCode。
能力对比
说到PDF Kit的功能,其实pdfService和PdfView这俩兄弟各有各的绝活。下面不是官方表格,纯属开发时的"碎碎念"总结:
- 打开和保存文档?都能搞,pdfService和PdfView都不怵。
- 释放文档?这俩都能释放,别担心内存泄漏。
- PDF转图片?都行,虽然我平时用得不多。
- 批注?都能加能删,产品要啥花样都能满足。
- 书签?pdfService能管,PdfView就别想了。
- 增删PDF页、加文本、加图片、改水印、页眉页脚啥的,pdfService全能,PdfView就负责老老实实预览。
- 判断PDF加没加密、解密?pdfService能查能解,PdfView还是只管看。
- 预览、搜索、监听回调?PdfView才是主场,pdfService就别凑热闹了。
总之,pdfService偏"动手能力",啥都能改能加能删,PdfView偏"观赏型",预览、翻页、搜索、批注体验都不错。实际开发时,哪个顺手用哪个,别死磕API文档,踩踩坑就明白了。
有时候真想让pdfService和PdfView合体,省得来回切换。可惜目前还得各司其职,凑合用吧。
约束与限制
- 支持区域:仅限中国大陆(不含港澳台)。
- 支持设备:仅支持真机(Phone、Tablet、PC/2in1),不支持模拟器。
打开和保存PDF文档
- 编辑PDF内容建议用
pdfService
。 - 仅预览、搜索、监听等场景推荐用
PdfView
。
常用API:
loadDocument(path: string, password?: string, onProgress?: Callback<number>): ParseResult
加载PDF。saveDocument(path: string, onProgress?: Callback<number>): boolean
保存PDF。
小故事: 第一次做"另存为"功能时,文件路径写错了,结果怎么点都没反应。后来才发现,沙箱路径和资源路径要分清楚,别把PDF写到只读目录里。
示例代码:
import { pdfService } from '@kit.PDFKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo } from '@kit.CoreFileKit';
@Entry
@Component
struct PdfPage {
private pdfDocument: pdfService.PdfDocument = new pdfService.PdfDocument();
private context = this.getUIContext().getHostContext() as Context;
private filePath = '';
@State saveEnable: boolean = false;
aboutToAppear(): void {
this.filePath = this.context.filesDir + '/input.pdf';
let res = fileIo.accessSync(this.filePath);
if(!res) {
// 工程目录src/main/resources/rawfile需有input.pdf
let content: Uint8Array = this.context.resourceManager.getRawFileContentSync('rawfile/input.pdf');
let fdSand = fileIo.openSync(this.filePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
fileIo.writeSync(fdSand.fd, content.buffer);
fileIo.closeSync(fdSand.fd);
}
this.pdfDocument.loadDocument(this.filePath);
}
build() {
Column() {
// 另存为PDF
Button('Save As').onClick(() => {
let outPdfPath = this.context.filesDir + '/testSaveAsPdf.pdf';
let result = this.pdfDocument.saveDocument(outPdfPath);
this.saveEnable = true;
hilog.info(0x0000, 'PdfPage', 'saveAsPdf %{public}s!', result ? 'success' : 'fail');
})
// 覆盖保存
Button('Save').enabled(this.saveEnable).onClick(() => {
let tempDir = this.context.tempDir;
let tempFilePath = tempDir + `/temp${Math.random()}.pdf`;
fileIo.copyFileSync(this.filePath, tempFilePath);
let pdfDocument: pdfService.PdfDocument = new pdfService.PdfDocument();
let loadResult = pdfDocument.loadDocument(tempFilePath, '');
if (loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
let result = pdfDocument.saveDocument(this.filePath);
hilog.info(0x0000, 'PdfPage', 'savePdf %{public}s!', result ? 'success' : 'fail');
}
})
}
}
}
添加、删除PDF页
- 支持插入空白页、合并其他PDF页、删除指定页。
常用API:
insertBlankPage(index, width, height)
插入空白页。getPage(index)
获取指定页对象。insertPageFromDocument(document, fromIndex, pageCount, index)
合并其他文档页。deletePage(index, count)
删除页。
小插曲: 有次测试同事说"怎么插入的页都在最后?"其实是index参数没理解透,插入位置要算准,不然用户体验很迷。
示例代码:
import { pdfService } from '@kit.PDFKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct PdfPage {
private pdfDocument: pdfService.PdfDocument = new pdfService.PdfDocument();
private context = this.getUIContext().getHostContext() as Context;
aboutToAppear(): void {
let filePath = this.context.filesDir + '/input.pdf';
this.pdfDocument.loadDocument(filePath);
}
build() {
Column() {
// 插入单个空白页
Button('insertBankPage').onClick(async () => {
let page = this.pdfDocument.getPage(0);
this.pdfDocument.insertBlankPage(2, page.getWidth(), page.getHeight());
let outPdfPath = this.context.filesDir + '/testInsertBankPage.pdf';
let result = this.pdfDocument.saveDocument(outPdfPath);
hilog.info(0x0000, 'PdfPage', 'insertBankPage %{public}s!', result ? 'success' : 'fail');
})
// 插入多个空白页
Button('insertSomeBankPage').onClick(async () => {
let page = this.pdfDocument.getPage(0);
for (let i = 0; i < 3; i++) {
this.pdfDocument.insertBlankPage(2, page.getWidth(), page.getHeight());
}
let outPdfPath = this.context.filesDir + '/testInsertSomeBankPage.pdf';
let result = this.pdfDocument.saveDocument(outPdfPath);
hilog.info(0x0000, 'PdfPage', 'insertSomeBankPage %{public}s!', result ? 'success' : 'fail');
})
// 合并其他PDF页
Button('insertPageFromDocument').onClick(async () => {
let pdfDoc = new pdfService.PdfDocument();
pdfDoc.loadDocument(this.context.filesDir + '/input2.pdf');
this.pdfDocument.insertPageFromDocument(pdfDoc, 1, 3, 0);
let outPdfPath = this.context.filesDir + '/testInsertPageFromDocument.pdf';
let result = this.pdfDocument.saveDocument(outPdfPath);
hilog.info(0x0000, 'PdfPage', 'insertPageFromDocument %{public}s!', result ? 'success' : 'fail');
})
// 删除页
Button('deletePage').onClick(async () => {
this.pdfDocument.deletePage(2, 2);
let outPdfPath = this.context.filesDir + '/testDeletePage.pdf';
let result = this.pdfDocument.saveDocument(outPdfPath);
hilog.info(0x0000, 'PdfPage', 'deletePage %{public}s!', result ? 'success' : 'fail');
})
}
}
}
预览PDF文档
- 支持页面跳转、缩放、单双页显示、适配、滚动、搜索、批注等。
- 需确保沙箱目录有PDF文件。
开发感受: 预览PDF时,最怕的就是"加载慢"或者"翻页卡"。建议用监听回调,给用户加个加载动画,体验会好很多。
示例代码:
import { pdfService, pdfViewManager, PdfView } from '@kit.PDFKit';
import { fileIo } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct Index {
private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
aboutToAppear(): void {
let context = this.getUIContext().getHostContext() as Context;
let dir = context.filesDir;
let filePath = dir + '/input.pdf';
let res = fileIo.accessSync(filePath);
if (!res) {
let content = context.resourceManager.getRawFileContentSync('rawfile/input.pdf');
let fdSand = fileIo.openSync(filePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
fileIo.writeSync(fdSand.fd, content.buffer);
fileIo.closeSync(fdSand.fd);
}
(async () => {
// 文档加载前注册监听
this.controller.registerPageCountChangedListener((pageCount: number) => {
hilog.info(0x0000, 'registerPageCountChanged-', pageCount.toString());
});
let loadResult1 = await this.controller.loadDocument(filePath);
})();
}
build() {
Row() {
PdfView({
controller: this.controller,
pageFit: pdfService.PageFit.FIT_WIDTH,
showScroll: true
})
.id('pdfview_app_view')
.layoutWeight(1);
}
.width('100%')
.height('100%')
}
}
PdfView 进阶用法
异步打开和保存PDF文档(Promise方式)
小故事: 有次遇到大文件,保存时UI直接卡死。后来才知道要用Promise异步,别让主线程等着,用户体验直接提升。
示例代码:
import { pdfService, PdfView, pdfViewManager } from '@kit.PDFKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct PdfPage {
private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
private context = this.getUIContext().getHostContext() as Context;
private loadResult: pdfService.ParseResult = pdfService.ParseResult.PARSE_ERROR_FORMAT;
aboutToAppear(): void {
let filePath = this.context.filesDir + '/input.pdf';
(async () => {
this.loadResult = await this.controller.loadDocument(filePath);
})()
}
build() {
Column() {
Button('savePdfDocument').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
let savePath = this.context.filesDir + '/savePdfDocument.pdf';
let result = await this.controller.saveDocument(savePath);
hilog.info(0x0000, 'PdfPage', 'savePdfDocument %{public}s!', result ? 'success' : 'fail');
}
})
PdfView({
controller: this.controller,
pageFit: pdfService.PageFit.FIT_WIDTH,
showScroll: true
})
.id('pdfview_app_view')
.layoutWeight(1);
}
.width('100%')
.height('100%')
}
}
设置PDF文档预览效果
开发趣事: 产品说"能不能像翻书一样双页显示?"我一开始以为很难,结果一行setPageLayout就搞定了。HarmonyOS的API有时候还挺贴心。
示例代码:
import { pdfService, PdfView, pdfViewManager } from '@kit.PDFKit';
@Entry
@Component
struct PdfPage {
private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
private context = this.getUIContext().getHostContext() as Context;
private loadResult: pdfService.ParseResult = pdfService.ParseResult.PARSE_ERROR_FORMAT;
aboutToAppear(): void {
let filePath = this.context.filesDir + '/input.pdf';
(async () => {
this.loadResult = await this.controller.loadDocument(filePath);
})()
}
build() {
Column() {
Row() {
Button('setPreviewMode').onClick(() => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
this.controller.setPageLayout(pdfService.PageLayout.LAYOUT_SINGLE); // 单页
this.controller.setPageContinuous(true); // 连续滚动
this.controller.setPageFit(pdfService.PageFit.FIT_PAGE); // 适配整页
}
})
Button('goTopage').onClick(() => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
this.controller.goToPage(10); // 跳转到第11页
}
})
Button('zoomPage2').onClick(() => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
this.controller.setPageZoom(2); // 放大2倍
}
})
}
PdfView({
controller: this.controller,
pageFit: pdfService.PageFit.FIT_WIDTH,
showScroll: true
})
.id('pdfview_app_view')
.layoutWeight(1);
}
}
}
搜索关键字与高亮
小插曲: 有用户反馈"搜索C++怎么没反应?"一查才发现,大小写和特殊字符要注意,API其实不区分大小写,但有些符号要转义。
示例代码:
import { pdfService, PdfView, pdfViewManager } from '@kit.PDFKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct PdfPage {
private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
private context = this.getUIContext().getHostContext() as Context;
private loadResult: pdfService.ParseResult = pdfService.ParseResult.PARSE_ERROR_FORMAT;
private searchIndex = 0;
private charCount = 0;
aboutToAppear(): void {
let filePath = this.context.filesDir + '/input.pdf';
(async () => {
this.loadResult = await this.controller.loadDocument(filePath);
})()
}
build() {
Column() {
Row() {
Button('searchKey').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
this.controller.searchKey('C++', (index: number) => {
this.charCount = index;
hilog.info(0x0000, 'PdfPage', 'searchKey %{public}s!', index + '');
})
}
})
Button('setSearchPrevIndex').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
if(this.searchIndex > 0) {
this.controller.setSearchIndex(--this.searchIndex);
}
}
})
Button('setSearchNextIndex').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
if(this.searchIndex < this.charCount) {
this.controller.setSearchIndex(++this.searchIndex);
}
}
})
Button('getSearchIndex').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
let curSearchIndex = this.controller.getSearchIndex();
hilog.info(0x0000, 'PdfPage', 'curSearchIndex %{public}s!', curSearchIndex + '');
}
})
Button('clearSearch').onClick(async () => {
if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
this.controller.clearSearch();
}
})
}
PdfView({
controller: this.controller,
pageFit: pdfService.PageFit.FIT_WIDTH,
showScroll: true
})
.id('pdfview_app_view')
.layoutWeight(1);
}
}
}
常见问题与建议
- 仅支持中国大陆真机,模拟器和港澳台暂不支持。
- 资源文件需提前放入rawfile目录并拷贝到沙箱。
- 编辑操作建议用pdfService,纯预览用PdfView。
- 保存/覆盖操作注意文件路径和权限。