Flutter PDF处理插件pdfrx_fork的使用

Flutter PDF处理插件pdfrx_fork的使用

注意事项

本插件是原始插件pdfrx的一个克隆版本,包含了一些优化代码以支持Flutter < 3.19。您可以查看原始插件在以下链接: 原始插件

pdfrx 是一个基于 pdfium 构建的丰富的快速PDF查看器实现。该插件支持Android、iOS、Windows、macOS、Linux,但不支持Web。

交互式演示

一个使用Flutter Web的演示站点: pdfrx

主要功能

  • 可缩放和滚动的PDF文档查看器
  • 支持PDF链接处理
  • 支持文档大纲(即书签)
  • 支持文本选择(仍在实验中)
  • 支持文本搜索
  • 高度可定制化
  • 多平台支持
    • Android
    • iOS
    • Windows
    • macOS
    • Linux(甚至可以在Raspberry PI上运行)
  • 三层API
    • 易用的Flutter小部件
      • PdfViewer
      • PdfDocumentViewBuilder
      • PdfPageView
    • 易用的PDF API
      • PdfDocument
    • pdfium绑定
      • 不鼓励使用,但你可以导入 package:pdfrx/src/pdfium/pdfium_bindings.dart

入门指南

以下片段展示了如何在资产中显示PDF文件:

class _MyAppState extends State<MyApp> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Pdfrx example'),
        ),
        body: PdfViewer.asset('assets/hello.pdf'),
      ),
    );
  }
}

安装

将以下内容添加到您的包的pubspec.yaml文件中,并执行flutter pub get

dependencies:
  pdfrx: ^0.0.53
Windows

确保启用开发人员模式。 在构建过程中,内部使用了符号链接,这需要启用开发者模式。如果不启用,可能会遇到错误,如 此链接 所示。

Web

现在会自动加载pdf.js,无需修改index.html。但您可以自定义下载URLs来设置PdfJsConfiguration.configuration

PdfJsConfiguration.configuration = const PdfJsConfiguration(
  pdfJsSrc: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.mjs',
  workerSrc: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js',
  cMapUrl: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/cmaps/',
);

请注意,pdf.js 4.X 版本尚未支持,请使用3.X版本。

macOS

对于macOS,Flutter应用默认限制其功能通过启用 App Sandbox。您可以根据配置编辑您的应用的权限文件。例如:

处理App Sandbox

最简单的方法是禁用App Sandbox,尽管这在发布应用时不推荐使用。另一种方法是使用 com.apple.security.files.user-selected.read-onlyfile_selector_macos。以下是示例代码:

<dict>
  <key>com.apple.security.app-sandbox</key>
  <true/>
  <key>com.apple.security.network.client</key>
  <true/>
</dict>

打开PDF文件

PdfViewer 支持以下方法打开特定介质上的PDF文件:

  • PdfViewer.asset
    • 打开Flutter资源中的PDF文件
  • PdfViewer.file
    • 打开本地文件中的PDF文件
  • PdfViewer.uri
    • 从URI(https://... 或相对路径)打开PDF文件
    • 在Flutter Web上,可能会被CORS阻止
处理密码保护的PDF文件

要支持密码保护的PDF文件,可以使用 passwordProvider 交互地提供密码:

PdfViewer.asset(
  'assets/test.pdf',
  // 设置密码提供者以显示密码对话框
  passwordProvider: _passwordDialog,
  ...
),

_passwordDialog 函数定义如下:

Future<String?> _passwordDialog() async {
  final textController = TextEditingController();
  return await showDialog<String>(
    context: context,
    barrierDismissible: false,
    builder: (context) {
      return AlertDialog(
        title: const Text('Enter password'),
        content: TextField(
          controller: textController,
          autofocus: true,
          keyboardType: TextInputType.visiblePassword,
          obscureText: true,
          onSubmitted: (value) => Navigator.of(context).pop(value),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(null),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(textController.text),
            child: const Text('OK'),
          ),
        ],
      );
    },
  );
}

PdfViewer 尝试打开一个密码保护的文档时,它会重复调用传递给 passwordProvider 的函数(第一次尝试除外),直到成功打开文档。如果该函数返回null,则查看器将放弃密码尝试,且该函数不再被调用。

第一次尝试使用空密码

默认情况下,第一次密码尝试使用空密码。这是因为加密的PDF文件通常使用空密码用于查看目的。这在大多数情况下是有用的,但如果需要使用作者密码,可以通过设置 firstAttemptByEmptyPassword 为false来禁用。

自定义化

您可以使用 PdfViewerParams 来自定义行为和视觉效果。

文本选择

文本选择功能仍在实验中,但您可以轻松启用它:

PdfViewer.asset(
  'assets/test.pdf',
  enableTextSelection: true,
  ...
),
PDF链接处理

要启用PDF文件中的链接,应设置 PdfViewerParams.linkWidgetBuilder

linkWidgetBuilder: (context, link, size) => Material(
  color: Colors.transparent,
  child: InkWell(
    onTap: () {
      // 处理URL或目的地
      if (link.url != null) {
        // TODO: 实现自己的isSecureUrl
        if (await isSecureUrl(link.url!)) {
          launchUrl(link.url!);
        }
      } else if (link.dest != null) {
        controller.goToDest(link.dest);
      }
    },
    hoverColor: Colors.blue.withOpacity(0.2),
  ),
),

对于URI,应在打开URI之前检查其有效性;示例代码只是弹出对话框询问是否打开URL。

对于目的地,您可以使用 PdfViewerController.goToDest 转到目的地。或者,您可以使用 PdfViewerController.calcMatrixForDest 获取矩阵。

文档大纲(书签)

PDF定义了文档大纲(PdfOutlineNode),有时也称为书签或索引。您可以使用 PdfDocument.loadOutline 访问它。

以下片段在 PdfViewerParams.onViewerReady 上获取它:

onViewerReady: (document, controller) async {
  outline.value = await document.loadOutline();
},

PdfOutlineNode 是树状结构的数据,更多信息见 示例代码

横向滚动视图

默认情况下,页面布局为垂直方向。您可以使用 PdfViewerParams.layoutPages 自定义布局逻辑:

layoutPages: (pages, params) {
  final height = pages.fold(0.0, (prev, page) => max(prev, page.height)) + params.margin * 2;
  final pageLayouts = <Rect>[];
  double x = params.margin;
  for (var page in pages) {
    pageLayouts.add(
      Rect.fromLTWH(
        x,
        (height - page.height) / 2, // 垂直居中
        page.width,
        page.height,
      ),
    );
    x += page.width + params.margin;
  }
  return PdfPageLayout(
    pageLayouts: pageLayouts,
    documentSize: Size(x, height),
  );
},
对面页

以下代码将展示“对面顺序布局”,这是PDF查看器应用中常用的布局方式:

/// 页面阅读顺序;true表示L到R,通常是书籍使用的顺序
var isRightToLeftReadingOrder = false;
/// 使用第一页作为封面页
var needCoverPage = true;

...

layoutPages: (pages, params) {
  final width = pages.fold(
      0.0, (prev, page) => max(prev, page.width));

  final pageLayouts = <Rect>[];
  final offset = needCoverPage ? 1 : 0;
  double y = params.margin;
  for (int i = 0; i < pages.length; i++) {
    final page = pages[i];
    final pos = i + offset;
    final isLeft = isRightToLeftReadingOrder
        ? (pos & 1) == 1
        : (pos & 1) == 0;

    final otherSide = (pos ^ 1) - offset;
    final h = 0 <= otherSide && otherSide < pages.length
        ? max(page.height, pages[otherSide].height)
        : page.height;

    pageLayouts.add(
      Rect.fromLTWH(
        isLeft
            ? width + params.margin - page.width
            : params.margin * 2 + width,
        y + (h - page.height) / 2,
        page.width,
        page.height,
      ),
    );
    if (pos & 1 == 1 || i + 1 == pages.length) {
      y += h + params.margin;
    }
  }
  return PdfPageLayout(
    pageLayouts: pageLayouts,
    documentSize: Size(
      (params.margin + width) * 2 + params.margin,
      y,
    ),
  );
},
显示滚动条

默认情况下,查看器不会显示任何滚动条或滚动拇指。您可以使用 PdfViewerParams.viewerOverlayBuilder 添加滚动拇指:

viewerOverlayBuilder: (context, size) => [
  // 在查看器右侧添加垂直滚动条
  PdfViewerScrollThumb(
    controller: controller,
    orientation: ScrollbarOrientation.right,
    thumbSize: const Size(40, 25),
    thumbBuilder:
        (context, thumbSize, pageNumber, controller) =>
            Container(
      color: Colors.black,
      // 在拇指上显示页码
      child: Center(
        child: Text(
          pageNumber.toString(),
          style: const TextStyle(color: Colors.white),
        ),
      ),
    ),
  ),
  // 在查看器底部添加水平滚动条
  PdfViewerScrollThumb(
    controller: controller,
    orientation: ScrollbarOrientation.bottom,
    thumbSize: const Size(80, 30),
    thumbBuilder:
        (context, thumbSize, pageNumber, controller) =>
            Container(
      color: Colors.red,
    ),
  ),
],
在每页底部添加页码

如果您想在每页底部添加页码,可以使用 PdfViewerParams.pageOverlayBuilder

pageOverlayBuilder: (context, pageRect, page) {
  return Align(
    alignment: Alignment.bottomCenter,
    child: Text(page.pageNumber.toString(),
    style: const TextStyle(color: Colors.red))),
},
加载指示器

PdfViewer.uri 可能需要很长时间下载PDF文件,您可能希望显示一些加载指示器。您可以使用 PdfViewerParams.loadingBannerBuilder

loadingBannerBuilder: (context, bytesDownloaded, totalBytes) {
  return Center(
    child: CircularProgressIndicator(
      // totalBytes可能在某些情况下不可用
      value: totalBytes != null ? bytesDownloaded / totalBytes : null,
      backgroundColor: Colors.grey,
    ),
  );
}

暗/夜模式支持

PdfViewer 本身没有原生的暗模式或夜模式支持,但可以使用 ColorFiltered 小部件轻松实现:

ColorFiltered(
  colorFilter: ColorFilter.mode(Colors.white, darkMode ? BlendMode.difference : BlendMode.dst),
  child: PdfViewer.file(filePath, ...),
),

该技巧最初由 pckimlong 引入。

其他功能

文本搜索

TextSearcher 是一个辅助类,帮助您在应用程序中实现文本搜索功能。

以下片段展示了 TextSearcher 的整体结构:

class _MainPageState extends State<MainPage> {
  final controller = PdfViewerController();
  // 创建一个 PdfTextSearcher 并添加一个监听器来更新GUI上的搜索结果变化
  late final textSearcher = PdfTextSearcher(controller)..addListener(_update);

  void _update() {
    if (mounted) {
      setState(() {});
    }
  }

  [@override](/user/override)
  void dispose() {
    // 释放 PdfTextSearcher
    textSearcher.removeListener(_update);
    textSearcher.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pdfrx example'),
      ),
      body: PdfViewer.asset(
        'assets/hello.pdf',
        controller: controller,
        params: PdfViewerParams(
          // 添加 pageTextMatchPaintCallback 以绘制搜索高亮
          pagePaintCallbacks: [
            textSearcher.pageTextMatchPaintCallback
          ],
        ),
      )
    );
  }
  ...
}

在此片段中,它执行以下操作:

  • 创建 TextSearcher 实例
  • 添加监听器(使用 PdfTextSearcher.addListener)来更新UI上的搜索结果变化
  • TextSearcher.pageTextMatchPaintCallback 添加到 PdfViewerParams.pagePaintCallbacks 以显示搜索匹配项

然后,您可以使用 TextSearcher.startTextSearch 在PDF文档中搜索文本:

textSearcher.startTextSearch('hello', caseInsensitive: true);

搜索将在后台运行,并通过监听器通知搜索进度。

您还可以使用以下函数导航用户到搜索匹配项:

  • TextSearcher.goToMatchOfIndex 转到指定索引的匹配项
  • TextSearcher.goToNextMatch 转到下一个匹配项
  • TextSearcher.goToPrevMatch 转到上一个匹配项

您可以在搜索期间(即使搜索正在进行时)通过 PdfTextRange 列表获取搜索结果:

for (final match in textSearcher.matches) {
  print(match.pageNumber);
  ...
}

您还可以取消后台搜索:

textSearcher.resetTextSearch();
PdfDocumentViewBuilder/PdfPageView

PdfPageView 是另一个仅显示一页的PDF小部件。它接受 PdfDocument 和页码以显示文档内的一页。

PdfDocumentViewBuilder 用于在小部件树内安全管理 PdfDocument,并接受 builder 参数以创建子小部件。

以下片段是这些小部件的典型用法:

PdfDocumentViewBuilder.asset(
  'asset/test.pdf',
  builder: (context, document) => ListView.builder(
    itemCount: document?.pages.length ?? 0,
    itemBuilder: (context, index) {
      return Container(
        margin: const EdgeInsets.all(8),
        height: 240,
        child: Column(
          children: [
            SizedBox(
              height: 220,
              child: PdfPageView(
                document: document,
                pageNumber: index + 1,
                alignment: Alignment.center,
              ),
            ),
            Text(
              '${index + 1}',
            ),
          ],
        ),
      );
    },
  ),
),
PdfDocument 管理

PdfDocumentViewBuilder 可以接受来自 PdfViewerPdfDocumentRef,以安全地共享相同的 PdfDocument 实例。更多信息见 示例/lib/thumbnails_view.dart

完整示例代码

以下是完整的示例代码:

import 'package:flutter/material.dart';
import 'package:pdfrx_fork/pdfrx_fork.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PDF Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'PDF Example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final uri = Uri.parse('https://pdfobject.com/pdf/sample.pdf');
    final params = PdfViewerParams(
      backgroundColor: Colors.white,
      loadingBannerBuilder: (context, bytesDownloaded, totalBytes) {
        return const Center(child: CircularProgressIndicator.adaptive());
      },
    );
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: PdfViewer.uri(uri, params: params),
    );
  }
}

更多关于Flutter PDF处理插件pdfrx_fork的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter PDF处理插件pdfrx_fork的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


pdfrx_fork 是一个用于在 Flutter 应用中处理 PDF 文件的插件。它是 pdfrx 插件的一个分支版本,提供了类似的功能,但可能在某些方面进行了改进或修复。使用 pdfrx_fork 插件,你可以在 Flutter 应用中加载、显示、以及处理 PDF 文件。

以下是使用 pdfrx_fork 插件的基本步骤:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 pdfrx_fork 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  pdfrx_fork: ^0.1.0 # 请使用最新版本

然后运行 flutter pub get 来安装依赖。

2. 导入库

在你的 Dart 文件中导入 pdfrx_fork

import 'package:pdfrx_fork/pdfrx_fork.dart';

3. 加载和显示 PDF 文件

你可以使用 PdfViewerController 来加载和显示 PDF 文件。以下是一个简单的示例:

import 'package:flutter/material.dart';
import 'package:pdfrx_fork/pdfrx_fork.dart';

class PdfViewerScreen extends StatefulWidget {
  @override
  _PdfViewerScreenState createState() => _PdfViewerScreenState();
}

class _PdfViewerScreenState extends State<PdfViewerScreen> {
  late PdfViewerController _pdfController;

  @override
  void initState() {
    super.initState();
    _pdfController = PdfViewerController();
    _loadPdf();
  }

  Future<void> _loadPdf() async {
    // 从网络加载 PDF 文件
    final pdfDocument = await PdfDocument.openAsset('assets/sample.pdf');
    _pdfController.loadDocument(pdfDocument);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PDF Viewer'),
      ),
      body: PdfViewer(
        controller: _pdfController,
      ),
    );
  }
}

4. 处理 PDF 文件

pdfrx_fork 提供了多种方法来处理 PDF 文件,例如:

  • 从网络加载 PDF

    final pdfDocument = await PdfDocument.openUrl('https://example.com/sample.pdf');
    
  • 从文件系统加载 PDF

    final pdfDocument = await PdfDocument.openFile('/path/to/sample.pdf');
    
  • 从 Asset 加载 PDF

    final pdfDocument = await PdfDocument.openAsset('assets/sample.pdf');
    
  • 获取 PDF 页数

    final pageCount = pdfDocument.pageCount;
    

5. 自定义 PDF 查看器

PdfViewer 组件提供了多种自定义选项,例如:

  • 缩放:你可以通过 PdfViewerController 控制 PDF 的缩放比例。
  • 页面导航:你可以使用 PdfViewerController 跳转到指定的页面。
  • 背景颜色:你可以通过 backgroundColor 属性设置背景颜色。

6. 释放资源

在使用完 PdfDocument 后,记得释放资源:

pdfDocument.close();

7. 处理错误

在加载或处理 PDF 文件时,可能会遇到错误。你可以使用 try-catch 来捕获和处理这些错误:

try {
  final pdfDocument = await PdfDocument.openAsset('assets/sample.pdf');
  _pdfController.loadDocument(pdfDocument);
} catch (e) {
  print('Failed to load PDF: $e');
}
回到顶部