Flutter PDF渲染与滚动插件pdf_render_scroll的使用

发布于 1周前 作者 yibo5220 来自 Flutter

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-onlyfile_selector_macos。这个选项比前一个更安全。

示例代码中展示了如何下载并预览互联网托管的PDF文件。它使用了com.apple.security.network.clientflutter_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

它还提供了goTogoToPage方法,可以滚动查看器以使文档的特定页面/区域可见:

[@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的多页视图

结合PdfDocumentLoaderPdfPageView,你可以显示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,
                )
              )
            )
          ),
        )
      ),
    ),
  );
}

自定义页面小部件

PdfDocumentLoaderPdfPageView都接受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

textureBuilderPdfPageTextureBuilder)生成直接对应于页面图像的实际小部件。生成的小部件可能因情况而异,但你可以通过其参数自定义行为。

该函数定义为:

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;
}

createImageIfNotAvailabledart: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

1 回复

更多关于Flutter PDF渲染与滚动插件pdf_render_scroll的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用pdf_render_scroll插件来渲染和滚动PDF文件的示例代码。这个插件允许你高效地渲染PDF文件并支持滚动功能。

首先,确保你已经在pubspec.yaml文件中添加了pdf_render_scroll依赖项:

dependencies:
  flutter:
    sdk: flutter
  pdf_render_scroll: ^x.y.z  # 替换为最新版本号

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

接下来,在你的Flutter项目中,你可以按照以下步骤使用pdf_render_scroll插件:

  1. 导入必要的包
import 'package:flutter/material.dart';
import 'package:pdf_render_scroll/pdf_render_scroll.dart';
import 'package:path_provider/path_provider.dart';
  1. 定义一个函数来加载本地或网络上的PDF文件

这里我们假设你有一个本地的PDF文件,你可以使用path_provider包来获取应用的文档目录路径,并将PDF文件放在那里。

Future<Uint8List?> loadPdfFromAssets(String assetPath) async {
  ByteData data = await rootBundle.load(assetPath);
  return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
}

或者,如果你有一个网络上的PDF文件,你可以使用http包来下载它:

import 'package:http/http.dart' as http;

Future<Uint8List?> loadPdfFromUrl(String url) async {
  final response = await http.get(Uri.parse(url));
  if (response.statusCode == 200) {
    return response.bodyBytes;
  } else {
    throw Exception('Failed to load PDF');
  }
}
  1. 使用PdfRenderScrollWidget来渲染PDF文件
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('PDF Render Scroll Example'),
        ),
        body: PdfViewerPage(),
      ),
    );
  }
}

class PdfViewerPage extends StatefulWidget {
  @override
  _PdfViewerPageState createState() => _PdfViewerPageState();
}

class _PdfViewerPageState extends State<PdfViewerPage> {
  late Uint8List pdfData;

  @override
  void initState() {
    super.initState();
    // 使用本地PDF文件
    loadPdfFromAssets('assets/sample.pdf').then((data) {
      setState(() {
        pdfData = data!;
      });
    });
    // 或者使用网络上的PDF文件
    // loadPdfFromUrl('https://example.com/sample.pdf').then((data) {
    //   setState(() {
    //     pdfData = data!;
    //   });
    // });
  }

  @override
  Widget build(BuildContext context) {
    return pdfData.isEmpty
        ? Center(child: CircularProgressIndicator())
        : PdfRenderScrollWidget(
            pdfData: pdfData,
            onError: (error) {
              // 处理加载PDF时的错误
              print('Error loading PDF: $error');
            },
          );
  }
}

在这个示例中,PdfRenderScrollWidget负责渲染PDF文件,并允许用户滚动查看内容。loadPdfFromAssets函数用于从应用的assets目录中加载PDF文件,而loadPdfFromUrl函数(被注释掉了)则用于从网络上加载PDF文件。

确保你的项目中的assets目录包含你要加载的PDF文件,并在pubspec.yaml中声明它:

flutter:
  assets:
    - assets/sample.pdf

这样,你就可以在Flutter应用中高效地渲染和滚动查看PDF文件了。

回到顶部