Flutter PDF处理插件pdfrx_fork的使用
Flutter PDF处理插件pdfrx_fork的使用
注意事项
本插件是原始插件pdfrx
的一个克隆版本,包含了一些优化代码以支持Flutter < 3.19。您可以查看原始插件在以下链接:
原始插件
pdfrx
是一个基于 pdfium
构建的丰富的快速PDF查看器实现。该插件支持Android、iOS、Windows、macOS、Linux,但不支持Web。
交互式演示
一个使用Flutter Web的演示站点:
主要功能
- 可缩放和滚动的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
- 不鼓励使用,但你可以导入
- 易用的Flutter小部件
入门指南
以下片段展示了如何在资产中显示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-only
和 file_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阻止
- 从URI(
处理密码保护的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
可以接受来自 PdfViewer
的 PdfDocumentRef
,以安全地共享相同的 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
更多关于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');
}