Flutter PDF渲染与滚动插件pdf_render_scroll的使用
Flutter PDF渲染与滚动插件pdf_render_scroll的使用
Introduction
pdf_render_scroll
是一个支持iOS(>= 8.0)、Android(>= API Level 21)和Web的PDF渲染实现。它提供了中间层的PDF渲染API以及易于使用的Flutter小部件。该插件在Web上支持鼠标滚轮滚动。
最简单的示例
以下代码片段展示了如何在资产文件中展示PDF文件:
[@override](/user/override)
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('最简单的PDF示例'),
),
backgroundColor: Colors.grey,
body: PdfViewer.openAsset('assets/hello.pdf')
)
);
}
安装
在项目的pubspec.yaml
文件中添加依赖,并执行flutter pub get
命令:
dependencies:
pdf_render_scroll: ^1.3.8
Web
对于Web平台,需要在index.html
文件中添加以下脚本标签:
<!-- 加载pdfjs文件 -->
<script
src="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.12.313/build/pdf.js"
type="text/javascript"
></script>
<script type="text/javascript">
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist@2.12.313/build/pdf.worker.min.js";
pdfRenderOptions = {
// cmap文件的基础URL
cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.12.313/cmaps/",
// cmap文件是否压缩
cMapPacked: true,
// 其他pdfjsLib.getDocument参数
// params: {}
};
</script>
iOS/Android
对于iOS和Android平台,无需额外操作。
macOS
对于macOS平台,存在两个显著问题:
- 资源访问尚未实现。
- 默认情况下,Flutter应用会限制其功能,通过启用App Sandbox。你可以根据你的配置编辑应用的权限文件。详情见讨论部分。
处理App Sandbox
要访问磁盘上的文件,可以在权限文件中将com.apple.security.app-sandbox
设置为false
。但这不推荐用于发布应用,因为它完全禁用了App Sandbox。
另一种选项是使用com.apple.security.files.user-selected.read-only
与file_selector_macos
。这个选项比前一个更安全。
示例代码中展示了如何下载并预览互联网托管的PDF文件。它使用了com.apple.security.network.client
与flutter_cache_manager
:
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
实际实现见Missing network support?和示例代码。
小部件
导入小部件库
首先,需要导入以下库:
import 'package:pdf_render_scroll/pdf_render_scroll_widgets.dart';
PdfViewer
PdfViewer
是一个可扩展的PDF文档查看器小部件,支持捏缩放。以下片段是最简单的使用方式:
[@override](/user/override)
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('pdf_render_scroll 示例应用'),
),
backgroundColor: Colors.grey,
body: PdfViewer.openAsset(
'assets/hello.pdf',
params: PdfViewerParams(pageNumber: 2), // 显示第2页
)
)
);
}
在此代码中,使用PdfViewer.openAsset
加载了一个资产PDF文件。还有PdfViewer.openFile
用于本地文件和PdfViewer.openData
用于PDF二进制数据的Uint8List
。
缺失网络支持?
常见的请求之一是类似PdfViewer.openUri
的功能。该插件没有此功能,但可以使用flutter_cache_manager
轻松实现:
FutureBuilder<File>(
future: DefaultCacheManager().getSingleFile(
'https://github.com/espresso3389/flutter_pdf_render_scroll/raw/master/example/assets/hello.pdf'),
builder: (context, snapshot) =>
snapshot.hasData
? PdfViewer.openFile(snapshot.data!.path)
: Container( /* 占位符 */),
)
PdfViewerParams
PdfViewerParams
包含用于自定义PdfViewer
的参数。它还配备了继承自InteractiveViewer
的参数,可以使用几乎所有的InteractiveViewer
参数。
PdfViewerController
PdfViewerController
可用于获取PDF文档中的页数。
goTo/goToPage/goToPointInPage
它还提供了goTo
和goToPage
方法,可以滚动查看器以使文档的特定页面/区域可见:
[@override](/user/override)
Widget build(BuildContext context) {
PdfViewerController? controller;
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('pdf_render_scroll 示例应用'),
),
backgroundColor: Colors.grey,
body: PdfViewer.openAsset(
'assets/hello.pdf',
params: PdfViewerParams(
// 在控制器完全初始化时调用
onViewerControllerInitialized: (PdfViewerController c) {
controller = c;
controller.goToPage(pageNumber: 3); // 滚动到第3页
}
)
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.first_page),
onPressed: () => controller.ready?.goToPage(pageNumber: 1),
),
FloatingActionButton(
child: Icon(Icons.last_page),
onPressed: () => controller.ready?.goToPage(pageNumber: controller.pageCount),
),
],
),
),
);
}
goToPointInPage
只是goToPage
的另一个版本,它也接受内部页面点和点锚定的位置。
以下片段将第1页的中心对齐到视图的中心,并放大到300%:
controller.goToPointInPage(
pageNumber: 1,
x: 0.5,
y: 0.5,
anchor: PdfViewerAnchor.center,
zoomRatio: 3.0,
);
如果设置x: 0, y: 0, anchor: PdfViewerAnchor.topLeft
,则行为与goToPage
相同。
setZoomRatio
setZoomRatio
方法用于更改缩放比例而不滚动视图(这并不完全是真实的,但几乎是)。
以下片段将缩放比例更改为2.0:
controller.setZoomRatio(2.0);
在缩放操作期间,它保持中心点在视图中居中。
以下片段说明了另一种用例,双击放大:
final controller = PdfViewerController();
TapDownDetails? doubleTapDetails;
...
GestureDetector(
// 支持双击手势
onDoubleTapDown: (details) => doubleTapDetails = details,
onDoubleTap: () => controller.ready?.setZoomRatio(
zoomRatio: controller.zoomRatio * 1.5,
center: doubleTapDetails!.localPosition,
),
child: PdfViewer.openAsset(
'assets/hello.pdf',
viewerController: controller,
...
使用GestureDetector
,它首先在onDoubleTapDown
捕获双击位置。然后,在onDoubleTap
中使用该位置作为缩放中心。
手势管理
PdfViewer
仅支持拖拽和平移缩放。要支持其他手势,可以像上面解释的那样包裹GestureDetector
。
页面装饰
PdfViewer
中显示的每一页默认具有使用BoxDecoration
的阴影。可以通过PdfViewerParams.pageDecoration
属性覆盖外观。
进一步的页面外观定制
PdfViewerParams.buildPagePlaceholder
用于自定义在加载页面内容之前显示的空白页。PdfViewerParams.buildPageOverlay
用于在每一页上叠加内容。
这两个函数定义为BuildPageContentFunc
:
typedef BuildPageContentFunc = Widget Function(
BuildContext context,
int pageNumber,
Rect pageRect);
第三个参数pageRect
是页面在视图世界坐标中的位置。
单页视图
以下片段展示了使用PdfDocumentLoader
小部件最简单的方式仅渲染PDF文档的一页。适用于显示PDF缩略图。
[@override](/user/override)
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('pdf_render_scroll 示例应用'),
),
backgroundColor: Colors.grey,
body: Center(
child: PdfDocumentLoader.openAsset(
'assets/hello.pdf',
pageNumber: 1,
pageBuilder: (context, textureBuilder, pageSize) => textureBuilder()
)
)
),
);
}
当然,PdfDocumentLoader
有以下工厂函数:
PdfDocumentLoader.openAsset
PdfDocumentLoader.openFile
PdfDocumentLoader.openData
使用ListView.builder的多页视图
结合PdfDocumentLoader
和PdfPageView
,你可以显示PDF文档的多页。在以下片段中,使用ListView.builder
实现了可滚动的PDF文档查看器。
PdfDocumentLoader
最重要的作用是管理PdfDocument
的生命周期,当小部件树即将被释放时,它会释放文档。
[@override](/user/override)
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('pdf_render_scroll 示例应用'),
),
backgroundColor: Colors.grey,
body: Center(
child: PdfDocumentLoader.openAsset(
'assets/hello.pdf',
documentBuilder: (context, pdfDocument, pageCount) => LayoutBuilder(
builder: (context, constraints) => ListView.builder(
itemCount: pageCount,
itemBuilder: (context, index) => Container(
margin: EdgeInsets.all(margin),
padding: EdgeInsets.all(padding),
color: Colors.black12,
child: PdfPageView(
pdfDocument: pdfDocument,
pageNumber: index + 1,
)
)
)
),
)
),
),
);
}
自定义页面小部件
PdfDocumentLoader
和PdfPageView
都接受pageBuilder
参数,如果你想自定义每页的视觉效果。
以下片段说明了这一点:
PdfPageView(
pageNumber: index + 1,
// pageSize是PDF页面大小,单位为pt
pageBuilder: (context, textureBuilder, pageSize) {
//
// 这说明如何使用其他小部件装饰页面图像
//
return Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
// 容器为每页添加阴影
Container(
margin: EdgeInsets.all(margin),
padding: EdgeInsets.all(padding),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(2, 2))
]),
// textureBuilder构建实际的页面图像
child: textureBuilder()),
// 在渲染页面底部添加页码
Text('${index + 1}', style: TextStyle(fontSize: 50))
],
);
},
)
textureBuilder
textureBuilder
(PdfPageTextureBuilder
)生成直接对应于页面图像的实际小部件。生成的小部件可能因情况而异,但你可以通过其参数自定义行为。
该函数定义为:
typedef PdfPageTextureBuilder = Widget Function({
Size? size,
PdfPagePlaceholderBuilder? placeholderBuilder,
bool backgroundFill,
double? renderingPixelRatio
});
因此,如果你想要生成精确大小的小部件,可以显式指定size
。
请注意,大小是在密度无关像素中。该函数负责基于设备的像素密度确定实际像素大小。
placeholderBuilder
是最终控制加载或失败情况下的“占位符”的最后手段。
/// 创建页面占位符,该占位符在页面加载或甚至页面加载失败时显示。
typedef PdfPagePlaceholderBuilder = Widget Function(Size size, PdfPageStatus status);
/// 页面加载状态。
enum PdfPageStatus {
/// 页面正在加载中。
loading,
/// 页面加载失败。
loadFailed,
}
PDF渲染API
以下片段说明了PdfDocument
的整体使用:
import 'package:pdf_render_scroll/pdf_render_scroll.dart';
...
// 使用openFile、openAsset或openData打开文档。
// 对于Web,文件名可以是从index.html开始的相对路径或任意URL,但受CORS影响。
PdfDocument doc = await PdfDocument.openAsset('assets/hello.pdf');
// 获取PDF文件中的页数
int pageCount = doc!.pageCount;
// 第一页是1
PdfPage page = await doc!.getPage(1);
// 对于render函数的返回值,请参见下文解释
PdfPageImage pageImage = await page.render();
// 现在,你可以访问pageImage!.pixels来获取原始RGBA数据
// ...
// 为以后使用创建dart:ui.Image缓存
await pageImage.createImageIfNotAvailable();
// 必须尽快释放PDFDocument实例。
doc!.dispose();
然后,你可以使用PdfPageImage
获取dart:ui.Image
中的实际RGBA图像。
要将图像嵌入到小部件树中,可以使用RawImage
:
[@override](/user/override)
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.all(10.0),
color: Colors.grey,
child: Center(
// 在使用imageIfAvailable之前,你应该调用createImageIfNotAvailable
child: RawImage(image: pageImage.imageIfAvailable, fit: BoxFit.contain))
)
);
}
如果你只是构建小部件树,最好使用更快更高效的PdfPageImageTexture
。
PdfDocument.openXXX
在PdfDocument
类中,有三个函数用于从真实文件、资产文件或内存数据中打开PDF。
// 从资产文件
PdfDocument docFromAsset = await PdfDocument.openAsset('assets/hello.pdf');
// 从文件
// 对于Web,文件名可以是从index.html开始的相对路径或任意URL,但受CORS影响。
PdfDocument docFromFile = await PdfDocument.openFile('/somewhere/in/real/file/system/file.pdf');
// 从PDF内存图像的Uint8List
PdfDocument docFromData = await PdfDocument.openData(data);
PdfDocument成员
PdfDocument
类概述:
class PdfDocument {
/// 文件路径,取决于打开的内容,可能是`asset:[ASSET_PATH]`或`memory:`。
final String sourceName;
/// PDF文档中的页数。
final int pageCount;
/// PDF主要版本。
final int verMajor;
/// PDF次要版本。
final int verMinor;
/// 确定PDF文件是否加密。
final bool isEncrypted;
/// 确定PDF文件是否允许复制内容。
final bool allowsCopying;
/// 确定PDF文件是否允许打印页面。
final bool allowsPrinting;
// 根据页码获取页面(页码从1开始)
Future<PdfPage> getPage(int pageNumber);
// 释放实例。
Future<void> dispose();
}
PdfPage成员
PdfPage
类概述:
class PdfPage {
final PdfDocument document; // 用于内部目的
final int pageNumber; // 页码(页码从1开始)
final double width; // 页面宽度(以点为单位;72-dpi时的像素大小)
final double height; // 页面高度(以点为单位;72-dpi时的像素大小)
// 渲染PDF页面的子区域。
Future<PdfPageImage> render({
int x = 0,
int y = 0,
int? width,
int? height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
bool allowAntialiasingIOS = false
});
}
render
函数从缩放后的fullWidth
x fullHeight
PDF页面图像中提取子区域(x,y)
- (x + width, y + height)
。所有坐标都是以像素为单位。
以下片段在300 dpi下渲染页面:
const scale = 300.0 / 72.0;
const fullWidth = page.width * scale;
const fullHeight = page.height * scale;
var rendered = page.render(
x: 0,
y: 0,
width: fullWidth.toInt(),
height: fullHeight.toInt(),
fullWidth: fullWidth,
fullHeight: fullHeight);
PdfPageImage成员
PdfPageImage
类概述:
class PdfPageImage {
/// 页码。第一页是1。
final int pageNumber;
/// 渲染区域左X坐标(以像素为单位)。
final int x;
/// 渲染区域顶Y坐标(以像素为单位)。
final int y;
/// 渲染区域宽度(以像素为单位)。
final int width;
/// 渲染区域高度(以像素为单位)。
final int height;
/// 渲染页面图像的全宽度(以像素为单位)。
final int fullWidth;
/// 渲染页面图像的全高度(以像素为单位)。
final int fullHeight;
/// PDF页面宽度(以点为单位;72-dpi时的像素大小)。
final double pageWidth;
/// PDF页面高度(以点为单位;72-dpi时的像素大小)。
final double pageHeight;
/// RGBA像素字节数组。
final Uint8List pixels;
/// 获取[dart:ui.Image]对象。
Future<Image> createImageIfNotAvailable() async;
/// 如果可用,则获取[dart:ui.Image]对象;否则为null。
/// 如果你想确保[dart:ui.Image]对象可用,调用[createImageIfNotAvailable]。
Image? get imageIfAvailable;
}
createImageIfNotAvailable
在dart:ui.Image
中生成图像缓存,imageIfAvailable
返回可用的缓存图像。
如果你只需要RGBA字节数组,可以使用pixels
为此目的。像素位于(x,y)
处,位于pixels[(x+y*width)*4]
。尽管这样做完全可以正常工作,但强烈建议不要直接修改内容。
PdfPageImageTexture成员
PdfPageImageTexture
利用Flutter的Texture
类实现比PdfPageImage
/RawImage
组合更快且节省资源的渲染。
class PdfPageImageTexture {
final PdfDocument pdfDocument;
final int pageNumber;
final int texId;
bool get hasUpdatedTexture;
PdfPageImageTexture({required this.pdfDocument, required this.pageNumber, required this.texId});
/// 创建新的Flutter [Texture]。使用后应调用[dispose]方法释放对象。
static Future<PdfPageImageTexture> create({required PdfDocument pdfDocument, required int pageNumber});
/// 释放对象。
Future<void> dispose();
/// 提取PDF页面的子矩形([x],[y],[width],[height]),缩放到[fullWidth] x [fullHeight]大小。
/// 如果[backgroundFill]为true,则在渲染页面内容之前用白色填充子矩形。
Future<bool> extractSubrect({
int x = 0,
int y = 0,
required int width,
required int height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
});
}
自定义页面布局
PdfViewerParams
有一个属性layoutPages
,用于自定义页面布局。
有时,当你在手机或平板电脑上使用横向模式并且需要让PDF适应屏幕中心时,可以使用以下代码来自定义PDF布局:
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
backgroundColor: Colors.white70,
body: PdfViewer.openAsset(
'assets/hello.pdf',
params: PdfViewerParams(
layoutPages: (viewSize, pages) {
List<Rect> rect = [];
final viewWidth = viewSize.width;
final viewHeight = viewSize.height;
final maxHeight = pages.fold<double>(0.0, (maxHeight, page) => max(maxHeight, page.height));
final ratio = viewHeight / maxHeight;
var top = 0.0;
for (var page in pages) {
final width = page.width * ratio;
final height = page.height * ratio;
final left = viewWidth > viewHeight ? (viewWidth / 2) - (width / 2) : 0.0;
rect.add(Rect.fromLTWH(left, top, width, height));
top += height + 8 /* padding */;
}
return rect;
},
),
),
);
}
更多关于Flutter PDF渲染与滚动插件pdf_render_scroll的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html