当前位置: 首页 > news >正文

PDF Kit 使用示例(HarmonyOS)

PDF Kit 使用示例(HarmonyOS)

前言

说起PDF,开发时总绕不开。最早做PDF相关功能,是帮同事搞个合同预览,结果一头雾水,踩了不少坑。后来用多了,发现HarmonyOS的PDF Kit其实挺顺手,能编辑、能预览、还能加批注,基本上开发需求都能覆盖。

这篇笔记就当是给后来人留个"避坑指南",也顺便记录下自己踩过的那些小坑和收获的经验。希望你用PDF Kit时,能少走点弯路,多点乐趣。

简介

PDF Kit(PDF服务)为HarmonyOS应用提供了丰富的PDF文档处理能力,包含 pdfServicePdfView 两大核心模块。

  • 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官方文档
http://www.lqws.cn/news/528535.html

相关文章:

  • 跟着AI学习C#之项目实战-电商平台 Day1
  • Web3解读:解锁去中心化网络的潜力
  • MessagesPlaceholder和多轮AI翻译助手实战
  • 【强化学习】《Reinforcement Learning: An Introduction》(第二版)概述
  • 杰理-可视化sdk-耳机灯效添加
  • Windows中使用createdump创建进程dump文件的基本用法
  • 开疆智能CCLinkIE转ModbusTCP网关连接PCA3200电能表配置案例
  • 人工智能编程三大核心流程详解--机器学习、神经网络、NLP自然语言处理
  • SQL Server 如何实现高可用和读写分离技术架构
  • SQL学习笔记3
  • AI矢量图与视频无痕修复:用Illustrator与After Effects解锁创作新维度
  • Android14音频子系统-Framework分析
  • Python 常用正则表达式大全
  • Spark 之 QueryStage
  • [Java实战]springboot3使用JDK21虚拟线程(四十)
  • 第十三章---软件工程过程管理
  • 【LLM】位置编码
  • vscode 回退代码版本
  • SQL变量声明与赋值 分支 循环
  • 信创国产化替代中的开发语言选择分析
  • 4.2_1朴素模式匹配算法
  • 6月份最新代发考试战报:思科华为HCIP HCSE 考试通过
  • Java四种拷贝方式总结!一文扫清所有拷贝问题
  • npm run dev报错
  • 软件安装——下载安装ollama
  • leetcode 65
  • Autosar方法论
  • 力扣2311:小于等于K的最长二进制子序列
  • 【TIDB】了解,MySQL和TiDB的取舍,差异
  • postman设置接口关联,实现参数化