鸿蒙ArkTs如何实现pdf预览功能?

news/2025/2/26 13:28:40

鸿蒙ArkTs如何实现pdf预览功能?

    • 前言
    • PDFKit运行示例代码报错
    • 真机运行
    • 先看效果
    • 一、预览本地pdf文件
    • 二、预览线上的pdf文件
    • 三、预览沙箱目录中pdf的文件(重点)
    • 效果中的整体代码
    • 总结
  • Harmony OS NEXT版本(接口及解决方案兼容API12版本或以上版本)

前言

在开发鸿蒙App时,你是否做过pdf预览功能。是否也和我一样碰壁了,来看看我遇到的问题,以及我是如何实现的吧。

PDFKit运行示例代码报错

the requested module '@hms:officeservice.PdfView' does not provide an export name 'pdfViewManager' which imported by 'xxxx'

真机运行

本来以为用真机就能运行了,没想到还是报错
在这里插入图片描述
那么下面来看看我是如何实现的吧

先看效果

在这里插入图片描述
视频转完gif,视觉上看起来有点卡,实际运行不卡。

pdf_16">一、预览本地pdf文件

预览本地的pdf文件很简单,使用Web组件加载即可。
pdf文件目录:harmonyApp\entry\src\main\resources\rawfile\test.pdf
具体代码如下:

import web_webview from '@ohos.web.webview';

@Entry
@Component
struct Index {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  build() {
    Column() {
      // src-本地pdf文件
      Web({ src: $rawfile('test.pdf'), controller: this.webviewController })
        .layoutWeight(1)
        .domStorageAccess(true)
    }
    .height('100%')
  }
}

pdf_41">二、预览线上的pdf文件

这里的线上的pdf文件是指可以在浏览器直接打开预览的pdf文件,还有一种是在浏览器打开是直接进入下载的,那么就需要我们进一步处理了,第三点有详解。
这样的文件预览也很简单,使用Web组件加载即可。
具体代码如下:

import web_webview from '@ohos.web.webview';

@Entry
@Component
struct Index {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  build() {
    Column() {
      // 线上pdf链接
      Web({ src: 'http://www.cztouch.com/upfiles/soft/testpdf.pdf', controller: this.webviewController })
        .layoutWeight(1)
        .domStorageAccess(true)
    }
    .height('100%')
  }
}

pdf_66">三、预览沙箱目录中pdf的文件(重点)

这种就比较麻烦了,有的pdf链接在浏览器打开直接跳转下载不会预览,那么就需要我们下载到沙箱目录中,再预览沙箱目录中的pdf文件。
我这里用到了一个pdfviewer工具,可从我的百度网盘免费获取
拿到文件夹后,放在以下目录:
项目目录:harmonyApp\entry\src\main\resources\rawfile
具体实现代码如下:

import router from '@ohos.router';
import web_webview from '@ohos.web.webview';
import { BusinessError, request } from '@kit.BasicServicesKit';
import showToast from '../../common/utils/ToastUtils';
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';

interface IBase64 {
  base64: string;
  fileName: string;
}

@Entry
@Component
struct Index2 {
  controller: web_webview.WebviewController = new web_webview.WebviewController()
  // pdf文件路径
  @State fileUrl: string = ''
  // 本地沙箱文件地址
  @State tempFilePath: string = ''
  // 是否显示按钮
  @State isShowBtn: boolean = true;

  build() {
    Stack() {
      Column() {
        // 页面内容
        Scroll(){
          Column(){
            if(this.tempFilePath){
              if(this.isShowBtn){
                Button('打开文件').onClick(()=>{
                  this.isShowBtn = false;
                })
              }else{
                Web({ src: $rawfile('pdfviewer/viewer.html'), controller: this.controller })
                  .onProgressChange((event)=>{
                    console.log("newProgress", event?.newProgress)
                  })
                  .domStorageAccess(true) // 设置是否开启文档对象模型存储接口(DOM Storage API)权限,默认未开启。
                  .onPageEnd(()=>{
                    let file = this.sandBoxPdfToBase64(this.tempFilePath);
                    this.controller.runJavaScript(`openFile("${file.base64}", "${file.fileName}")`);
                  })
              }
            }
          }.width('100%').height('100%')
        }
        .edgeEffect(EdgeEffect.Fade)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.TopStart)
      }
      .height('100%')
      .backgroundColor(Color.White)
    }
  }

  // 沙箱pdf文件转base64方法
  sandBoxPdfToBase64(url: string) {
    let file = fs.openSync(url, fs.OpenMode.READ_WRITE); // 打开文件
    let stat = fs.statSync(url); // 获取文件状态
    let buf = new ArrayBuffer(stat.size); // 创建一个ArrayBuffer对象
    let base64 = new util.Base64Helper(); // 实例化Base64Helper
    let num = fs.readSync(file.fd, buf); // 读取文件
    let data = base64.encodeSync(new Uint8Array(buf.slice(0, num))) //  转换成Uint8Array
    let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
    let retStr = textDecoder.decodeWithStream(data, { stream: false }); // 可以把Uint8Array转码成base64
    let fileName = file.name
    fs.closeSync(file);
    return { base64: retStr, fileName: fileName } as IBase64;
  }

  // 下载pdf文件,获取沙箱文件目录
  getTempFile(fileUrl:string){
    let context = getContext(this) as common.UIAbilityContext;
    const fileFullName = fileUrl.split('/')[fileUrl.split('/').length - 1]
    let tempFilePath = `${context.filesDir}/${fileFullName}`;
    //文件如果已经存在,就删除
    if (fs.accessSync(tempFilePath)) {
      fs.unlink(tempFilePath)
    }
    request.downloadFile(getContext(), { url: fileUrl,filePath: tempFilePath }).then((data: request.DownloadTask) => {
      let downloadTask: request.DownloadTask = data;
      let progressCallback = (receivedSize: number, totalSize: number) => {
        // 这里可以自行编写下载进度条
        showToast(`下载大小${receivedSize},总大小${totalSize}`);
      };
      let completeCallback = ()=>{
        showToast("下载完毕");
        this.tempFilePath = tempFilePath;
      }
      downloadTask.on('progress', progressCallback);
      downloadTask.on('complete', completeCallback)
    }).catch((err: BusinessError) => {
      console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
    })
  }

  // 组件生命周期:组件即将出现时回调该接口
  aboutToAppear() {
    console.log('进入页面')
    // 你的pdf链接
    this.fileUrl = (router.getParams() as Record<string, string>).url || '';
    this.getTempFile((router.getParams() as Record<string, string>).url as string);
  }
}

这里附有将pdf文件下载到沙箱目录代码,可选择使用(不必须)。

效果中的整体代码

import web_webview from '@ohos.web.webview';
import promptAction from '@ohos.promptAction'
import { BusinessError, request } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { util } from '@kit.ArkTS';

// pdf页面tab接口
interface pageTab {
  name:string;
}

interface IBase64 {
  base64: string;
  fileName: string;
}

/**
 * pdfPage的ViewModel
 */
class PdfPageModel {

  // 当前索引
  curTabIndex:number = 0;
  // pdf页面tab
  tabList:pageTab[] = [
    { name:'预览本地PDF文件' },
    { name:'预览网络PDF文件' },
    { name:'预览沙箱PDF文件' },
  ];

  // 网络文件
  fileUrl: string = 'http://www.cztouch.com/upfiles/soft/testpdf.pdf'
  // 本地沙箱文件地址
  tempFilePath: string = ''

  constructor() {
  }

  // 沙箱pdf文件转base64方法
  sandBoxPdfToBase64(url: string) {
    let file = fs.openSync(url, fs.OpenMode.READ_WRITE); // 打开文件
    let stat = fs.statSync(url); // 获取文件状态
    let buf = new ArrayBuffer(stat.size); // 创建一个ArrayBuffer对象
    let base64 = new util.Base64Helper(); // 实例化Base64Helper
    let num = fs.readSync(file.fd, buf); // 读取文件
    let data = base64.encodeSync(new Uint8Array(buf.slice(0, num))) //  转换成Uint8Array
    let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
    let retStr = textDecoder.decodeWithStream(data, { stream: false }); // 可以把Uint8Array转码成base64
    let fileName = file.name
    fs.closeSync(file);
    return { base64: retStr, fileName: fileName } as IBase64;
  }

  // 下载pdf文件,获取沙箱文件目录
  getTempFile(fileUrl:string){
    let context = getContext(this) as common.UIAbilityContext;
    const fileFullName = fileUrl.split('/')[fileUrl.split('/').length - 1]
    let tempFilePath = `${context.filesDir}/${fileFullName}`;
    //文件如果已经存在,就删除
    if (fs.accessSync(tempFilePath)) {
      fs.unlink(tempFilePath)
    }
    request.downloadFile(getContext(), { url: fileUrl,filePath: tempFilePath }).then((data: request.DownloadTask) => {
      let downloadTask: request.DownloadTask = data;
      let progressCallback = (receivedSize: number, totalSize: number) => {
        // 这里可以自行编写下载进度条
        // showToast(`下载大小${receivedSize},总大小${totalSize}`);
      };
      let completeCallback = ()=>{
        // showToast("下载完毕");
        this.tempFilePath = tempFilePath;
      }
      downloadTask.on('progress', progressCallback);
      downloadTask.on('complete', completeCallback)
    }).catch((err: BusinessError) => {
      console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
    })
  }

  // tab切换
  switchTab(index:number){
    this.curTabIndex = index;
    if(index === 2 && !this.tempFilePath){
      try {
        promptAction.showDialog({
          title: '温馨提示',
          message: '有些pdf线上链接是经过第三方加密过的,在浏览器访问时不能直接预览,直接走的是下载的pdf文件链接,可以采用这种方式,先下载在沙箱目录中,然后再预览沙箱中的pdf文件',
          buttons: [
            {
              text: '知道了',
              color: '#000000'
            }
          ]
        }, (err, data) => {
          if (err) {
            console.error('showDialog err: ' + err);
            return;
          }
          console.info('showDialog success callback, click button: ' + data.index);
        });
      } catch (error) {
        console.error(`Failed to show dialog. Code: ${error.code}, message: ${error.message}`);
      }
      this.getTempFile(this.fileUrl);
    }
  }
}




@Entry
@Component
struct PdfPage {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();
  @State vm: PdfPageModel = new PdfPageModel();

  // 验证是否选中
  VerifySelectedFun( curIndex:number , itemIndex:number ):boolean{
    return curIndex == itemIndex
  }

  aboutToAppear(): void {
    try {
      promptAction.showDialog({
        title: '温馨提示',
        message: '在模拟器中运行,首次加载会出现黑屏,但来回切换几次tab标签就好了,有条件的建议使用真机运行,不会有这样的问题',
        buttons: [
          {
            text: '知道了',
            color: '#000000'
          }
        ]
      }, (err, data) => {
        if (err) {
          console.error('showDialog err: ' + err);
          return;
        }
        console.info('showDialog success callback, click button: ' + data.index);
      });
    } catch (error) {
      console.error(`Failed to show dialog. Code: ${error.code}, message: ${error.message}`);
    }
  }

  build() {
    Stack() {
      Column() {
        // tab标签条
        Row(){
          Scroll(){
            Row(){
              ForEach(this.vm.tabList,(item:pageTab,index)=>{
                Row(){
                  if(this.VerifySelectedFun(this.vm.curTabIndex,index)){
                    Stack(){
                      Row(){}
                      .width(40)
                      .height(10)
                      .borderRadius(20)
                      .offset({y:7})
                      .linearGradient({angle:89.11,colors:[
                        ['rgba(255, 255, 255, 0.55)',0.0682],
                        ['rgba(217, 217, 217, 0)',1]
                      ]})
                      Text(item.name)
                        .fontSize(18)
                        .fontColor($r('app.color.primary_theme_color'))
                        .fontWeight(600)
                        .height('100%')
                    }

                  }else{
                    Text(item.name)
                      .fontSize(16)
                        // .fontColor($r('app.color.font_color_default'))
                      .fontWeight(400)
                  }
                }
                .height('100%')
                .justifyContent(FlexAlign.Start)
                .padding({ left: index == 0 ? 0 : 20 })
                .onClick(()=>{
                  this.vm.switchTab(index)
                })
              })
            }
          }.edgeEffect(EdgeEffect.Fade)
          .layoutWeight(1)
          .align(Alignment.Center)
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
        }.width('100%').height(50).justifyContent(FlexAlign.Start)
        .padding({left:16,right:16})
        .backgroundColor(Color.White)
        // 页面内容
        Scroll(){
          Column(){
            if(this.vm.curTabIndex === 0 ){
              // web组件加载本地pdf文件
              Web({ src: $rawfile('Git.pdf'), controller: this.webviewController})
                .domStorageAccess(true)
                .onProgressChange((event)=>{
                  console.log("newProgress", event?.newProgress)
                })
            }else if(this.vm.curTabIndex === 1){
              // web组件加载网络pdf文件
              Web({ src: 'http://www.cztouch.com/upfiles/soft/testpdf.pdf', controller: this.webviewController })
                .layoutWeight(1)
                .domStorageAccess(true)
                .onProgressChange((event)=>{
                  console.log("newProgress", event?.newProgress)
                })
            }else if(this.vm.curTabIndex === 2){
              if(this.vm.tempFilePath){
                Web({ src: $rawfile('pdfviewer/viewer.html'), controller: this.webviewController })
                  .onProgressChange((event)=>{
                    console.log("newProgress", event?.newProgress)
                  })
                  .domStorageAccess(true) // 设置是否开启文档对象模型存储接口(DOM Storage API)权限,默认未开启。
                  .onPageEnd(()=>{
                    let file = this.vm.sandBoxPdfToBase64(this.vm.tempFilePath);
                    this.webviewController.runJavaScript(`openFile("${file.base64}", "${file.fileName}")`);
                  })
              }
            }
          }.padding({ left: 16, right: 16, bottom: 16 })
        }
        .edgeEffect(EdgeEffect.Fade)
        .width('100%')
        .layoutWeight(1)
        .align(Alignment.TopStart)
      }
      .height('100%')
      .backgroundColor('#F5F5F5')
      .padding({ bottom: 16 })
    }
  }
}

总结

总体来说就是使用Web组件加载pdf文件,在模拟器中运行,首次运行会黑屏,不过来回切换一下tab页就好了,真机运行没有问题。
为啥要存到沙箱中再预览,岂不是多此一举?
当然不是,因为有的pdf文件是通过第三方加密过的,在浏览器打开链接时,是不能直接预览的,而是直接走下载了。这时,就需要先存到沙箱目录中再预览。

有需要的朋友,拿走不谢,求赞,求赞,求赞~
关注我不迷路,不定时分享鸿蒙难点亮点

Harmony OS NEXT版本(接口及解决方案兼容API12版本或以上版本)


http://www.niftyadmin.cn/n/5868765.html

相关文章

Project Reactor中 map、flatMap、concatMap 和 flatMapSequential 的区别

在 Project Reactor&#xff08;Reactive Streams 的实现库&#xff0c;常用于 Spring WebFlux&#xff09;中&#xff0c;map、flatMap、concatMap 和 flatMapSequential 是常用的操作符&#xff0c;但它们的功能和行为有显著区别。以下是它们的详细对比&#xff1a; 1. 功能对…

SOME/IP-SD -- 协议英文原文讲解2

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 5.1.2.2 S…

Para-Lane: 首个真实世界多车道数据集,目的评估自动驾驶系统中的新型视角合成能力。

2025-02-22&#xff0c;阿里巴巴集团菜鸟自动驾驶实验室和百度研究院共同创建了一个名为 Para-Lane 的真实世界多车道数据集。该数据集目的评估自动驾驶系统中的新型视角合成&#xff08;NVS&#xff09;能力&#xff0c;通过提供大量真实世界的数据&#xff0c;弥补了现有合成…

【大模型】Ubuntu下 fastgpt 的部署和使用

前言 本次安装的版本为 fastgpt:v4.8.8-fix2。 最新版本fastgpt:v4.8.20-fix2 问答时报错&#xff0c;本着跑通先使用起来&#xff0c;就没有死磕下去&#xff0c;后面bug解了再进行记录。   github连接&#xff1a;https://github.com/labring/FastGPT fastgpt 安装说明&…

jdk21下载、安装(Windows、Linux、macOS)

Windows 系统 1. 下载安装 访问 Oracle 官方 JDK 下载页面 或 OpenJDK 下载页面&#xff0c;根据自己的系统选择合适的 Windows 版本进行下载&#xff08;通常选择 .msi 安装包&#xff09;。 2. 配置环境变量 右键点击 “此电脑”&#xff0c;选择 “属性”。 在左侧导航栏…

vLLM专题(十四)-自动前缀缓存

一、介绍 自动前缀缓存(Automatic Prefix Caching,简称 APC)缓存现有查询的 KV 缓存,以便新查询如果与现有查询共享相同的前缀,可以直接重用 KV 缓存,从而跳过共享部分的计算。 注意 有关 vLLM 如何实现 APC 的技术细节,请参阅此处。 二、在 vLLM 中启用 APC 在 vLLM …

防火墙双机热备---VRRP,VGMP,HRP(超详细)

双机热备技术-----VRRP&#xff0c;VGMP&#xff0c;HRP三个组成 注&#xff1a;与路由器VRRP有所不同&#xff0c;路由器是通过控制开销值控制数据包流通方向 防火墙双机热备&#xff1a; 1.主备备份模式 双机热备最大的特点就是防火墙提供了一条专门的备份通道&#xff08;心…

low rank decomposition如何用于矩阵的分解

1. 什么是矩阵分解和低秩分解 矩阵分解是将一个矩阵表示为若干结构更简单或具有特定性质的矩阵的组合或乘积的过程。低秩分解&#xff08;Low Rank Decomposition&#xff09;是其中一种方法&#xff0c;旨在将原矩阵近似为两个或多个秩较低的矩阵的乘积&#xff0c;从而降低复…