PDF Kit 使用示例(HarmonyOS)

架构师手记
• 阅读 3

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.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。
  • 保存/覆盖操作注意文件路径和权限。

参考资料

点赞
收藏
评论区
推荐文章
源码补丁神器—patch-package
一、背景vue项目中使用第三方插件预览pdf,书写业务代码完美运行,pdf文件内容正常预览无问题。后期需求有变,业务需求增加电子签章功能。这个时候pdf文件的内容可以显示出来,但是公司的电子签章无法显示。这令人沮丧,因为已经编写了许多特定于此依赖项的代码,
移动端提高pdf预览清晰度
背景:移动端预览PDF文件,通用的解决方案是使用vuepdf插件,其内置pdf.js,原理是基于HTML5的标签,通过将PDF文件转换为图片或来实现对PDF文件的预览,插件好使没毛病😆,但是如果我们的需求是要在移动端预览内容很密集的文件时,预览效果就不理
陈杨 陈杨
4星期前
鸿蒙5开发宝藏案例分享---一多开发实例(短视频)
🌟【干货预警】今天在鸿蒙开发者文档里挖到宝了!原来官方早就藏了这么多"一多开发"的实战案例,难怪我之前的跨端适配总踩坑...这就把最新发现的短视频开发秘籍整理分享给大家,手把手教你用一套代码搞定手机/平板/折叠屏!一、开篇唠唠嗑最近被HarmonyOS的
陈杨 陈杨
4星期前
鸿蒙5开发宝藏案例分享---一多开发实例(即时通讯)
✨鸿蒙"一多"开发宝藏指南:原来官方案例还能这么玩!✨大家好呀!我是刚在鸿蒙开发路上踩完坑的某不知名码农,今天要给大家分享一个重大发现——原来HarmonyOS官方早就给我们准备好了超多实用开发案例!尤其是那个让无数人头疼的"一次开发多端部署",官方竟然悄
陈杨 陈杨
4星期前
鸿蒙5开发宝藏案例分享---应用接续提升内容发布体验
🌟【开发经验分享】鸿蒙应用接续功能实战:这些隐藏案例助你实现跨设备丝滑流转!各位开发者小伙伴们好呀今天在肝项目时意外解锁了HarmonyOS的一个"宝藏技能"——应用接续功能!官方文档里其实藏着超多实用案例,但很多同学可能没注意到。作为踩过无数坑的过来人
陈杨 陈杨
1天前
鸿蒙5开发宝藏案例分享---切面编程实战揭秘
鸿蒙切面编程(AOP)实战指南:隐藏的宝藏功能大揭秘!大家好!今天在翻鸿蒙开发者文档时,意外发现了官方埋藏的「切面编程」宝藏案例!实际开发中这些技巧能大幅提升效率,却很少被提及。下面用最直白的语言代码,带大家玩转HarmonyOS的AOP黑科技!一、什么
陈杨 陈杨
1天前
鸿蒙5开发宝藏案例分享---点击响应时延分析
鸿蒙宝藏大发现!官方隐藏的实战案例,开发效率直接翻倍🚀大家好呀!最近在折腾鸿蒙开发时,意外挖到了华为官方的案例宝藏库!原来HarmonyOS文档里藏了近百个场景化案例,覆盖了布局适配、性能优化、动效实现等高频需求。这些案例不仅提供完整代码,还有避坑指南,
架构师手记
架构师手记
Lv1
就算步伐很小,也要步步前进。
文章
2
粉丝
0
获赞
0