PDF Kit 使用示例(HarmonyOS)

2025-06-28 13:15:01
113次阅读
0个评论

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

参考资料

收藏00

登录 后评论。没有帐号? 注册 一个。