PDF Kit 使用示例(HarmonyOS)
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.pdflet 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() {// 另存为PDFButton('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。
- 保存/覆盖操作注意文件路径和权限。
参考资料
- PDF Kit官方文档