pdf_render is a PDF renderer implementation that supports iOS (>= 8.0), Android (>= API Level 21), and Web. It provides you with intermediate PDF rendering APIs and also easy-to-use Flutter Widgets.
The following fragment illustrates the easiest way to show a PDF file in assets:
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Easiest PDF sample'),
),
backgroundColor: Colors.grey,
body: PdfViewer.openAsset('assets/hello.pdf')
)
);
}
Add this to your package's pubspec.yaml
file and execute flutter pub get
:
dependencies:
pdf_render: ^1.0.18
For Web, you should add <script>
tags on your index.html
:
The plugin now utilizes PDF.js to support Flutter Web (still very early stage of implementation).
To use the Flutter Web support, you should add the following code just before <script src="main.dart.js" type="application/javascript"></script>
inside index.html
:
<!-- IMPORTANT: load pdfjs files -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.js" type="text/javascript"></script>
<script type="text/javascript">
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js";
pdfRenderOptions = {
// where cmaps are downloaded from
cMapUrl: 'https://cdn.jsdelivr.net/npm/[email protected]/cmaps/',
// The cmaps are compressed in the case
cMapPacked: true,
// any other options for pdfjsLib.getDocument.
// params: {}
}
// To workaround the recent breaking change on Flutter Web, you can set workaround_for_flutter_93615 to true:
//window.workaround_for_flutter_93615 = true;
</script>
You can use any URL that specify PDF.js
distribution URL.
cMapUrl
indicates cmap files base URL and cMapPacked
determines whether the cmap files are compressed or not.
Recently, Flutter Web changes ImageDescriptor.raw's behavior. And it results in PDF rendering breakage.
To workaround the issue, you should uncomment the following line on index.html
(that is already shown above):
window.workaround_for_flutter_93615 = true;
For more info, see the following issues:
- espresso3389/flutter_pdf_render: Web: PDF rendering distorted since Flutter Master 2.6.0-12.0.pre.674 #60
- flutter/flutter: [Web] Regression in Master - PDF display distorted due to change in BMP Encoder #93615
For iOS and Android, no additional task needed.
For macOS, there are two notable issues:
- Asset access is not working yet; see Flutter issue #47681: [macOS] add lookupKeyForAsset to FlutterPluginRegistrar
- Flutter app restrict its capability by enabling sandbox by default. You can change the behavior by editing either
Runner/Release.entitlements
orRunner/DebugProfile.entitlements
depending on your configuration.
The easiest option to access files on your disk, set com.apple.security.app-sandbox
to false
on your entitlements file though it is not recommended for releasing apps.
Another option is to use com.apple.security.files.user-selected.read-only
along with file_selector_macos. The option is better in security than the previous option.
Anyway, the example code for the plugin illustrates how to download and preview internet hosted PDF file. It uses
com.apple.security.network.client
along with flutter_cache_manager:
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
For the actual implementation, see Missing network support? and the example code.
Firstly, you must add the following import:
import 'package:pdf_render/pdf_render_widgets.dart';
PdfViewer is an extensible PDF document viewer widget which supports pinch-zoom.
The following fragment is a simplest use of the widget:
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Pdf_render example app'),
),
backgroundColor: Colors.grey,
// You can use either PdfViewer.openFile, PdfViewer.openAsset, or PdfViewer.openData
body: PdfViewer.openAsset(
'assets/hello.pdf',
params: PdfViewerParams(pageNumber: 2), // show the page-2
)
)
);
}
In the code above, the code uses PdfViewer.openAsset to load a asset PDF file. There are also PdfViewer.openFile for local file and PdfViewer.openData for Uint8List
of PDF binary data.
A frequent feature request is something like PdfViewer.openUri
. The plugin does not have it but it's easy to implement it with flutter_cache_manager:
FutureBuilder<File>(
future: DefaultCacheManager().getSingleFile(
'https://github.com/espresso3389/flutter_pdf_render/raw/master/example/assets/hello.pdf'),
builder: (context, snapshot) => snapshot.hasData
? PdfViewer.openFile(snapshot.data!.path)
: Container( /* placeholder */),
)
PdfViewerParams contains parameters to customize PdfViewer.
It also equips the parameters that are inherited from InteractiveViewer. You can use almost all parameters of InteractiveViewer.
PdfViewerController can be used to obtain number of pages in the PDF document. It also provide goTo and goToPage methods that you can scroll the viewer to make certain page/area of the document visible:
@override
Widget build(BuildContext context) {
PdfViewerController? controller;
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Pdf_render example app'),
),
backgroundColor: Colors.grey,
body: PdfViewer.openAsset(
'assets/hello.pdf',
params: PdfViewerParams(
// called when the controller is fully initialized
onViewerControllerInitialized: (PdfViewerController c) {
controller = c;
controller.goToPage(pageNumber: 3); // scrolling animation to page 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),
),
],
),
),
);
}
Each page shown in PdfViewer is by default has drop-shadow using BoxDecoration. You can override the appearance by PdfViewerParams.pageDecoration property.
PdfViewerParams.buildPagePlaceholder is used to customize the white blank page that is shown before loading the page contents.
PdfViewerParams.buildPageOverlay is used to overlay something on every page.
Both functions are defined as BuildPageContentFunc:
typedef BuildPageContentFunc = Widget Function(
BuildContext context,
int pageNumber,
Rect pageRect);
The third parameter, pageRect
is location of page in viewer's world coordinates.
The following fragment illustrates the easiest way to render only one page of a PDF document using PdfDocumentLoader widget. It is suitable for showing PDF thumbnail.
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Pdf_render example app'),
),
backgroundColor: Colors.grey,
body: Center(
child: PdfDocumentLoader.openAsset(
'assets/hello.pdf',
pageNumber: 1,
pageBuilder: (context, textureBuilder, pageSize) => textureBuilder()
)
)
),
);
}
Of course, PdfDocumentLoader has the following factory functions:
Using PdfDocumentLoader in combination with PdfPageView, you can show multiple pages of a PDF document. In the following fragment, ListView.builder
is utilized to realize scrollable PDF document viewer.
The most important role of PdfDocumentLoader is to manage life time of PdfDocument and it disposes the document when the widget tree is going to be disposed.
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Pdf_render example app'),
),
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,
)
)
)
),
)
)
),
);
}
Both PdfDocumentLoader and PdfPageView accepts pageBuilder
parameter if you want to customize the visual of each page.
The following fragment illustrates that:
PdfPageView(
pageNumber: index + 1,
// pageSize is the PDF page size in pt.
pageBuilder: (context, textureBuilder, pageSize) {
//
// This illustrates how to decorate the page image with other widgets
//
return Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
// the container adds shadow on each page
Container(
margin: EdgeInsets.all(margin),
padding: EdgeInsets.all(padding),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(2, 2))
]),
// textureBuilder builds the actual page image
child: textureBuilder()),
// adding page number on the bottom of rendered page
Text('${index + 1}', style: TextStyle(fontSize: 50))
],
);
},
)
textureBuilder
(PdfPageTextureBuilder) generates the actual widget that directly corresponding to the page image. The actual widget generated may vary upon the situation. But you can of course customize the behavior of the function with its parameter.
The function is defined as:
typedef PdfPageTextureBuilder = Widget Function({
Size? size,
PdfPagePlaceholderBuilder? placeholderBuilder,
bool backgroundFill,
double? renderingPixelRatio
});
So if you want to generate widget of an exact size, you can specify size
explicitly.
Please note that the size is in density-independent pixels. The function is responsible for determining the actual pixel size based on device's pixel density.
placeholderBuilder
is the final resort that controls the "placeholder" for loading or failure cases.
/// Creates page placeholder that is shown on page loading or even page load failure.
typedef PdfPagePlaceholderBuilder = Widget Function(Size size, PdfPageStatus status);
/// Page loading status.
enum PdfPageStatus {
/// The page is currently being loaded.
loading,
/// The page load failed.
loadFailed,
}
The following fragment illustrates overall usage of PdfDocument:
import 'package:pdf_render/pdf_render.dart';
...
// Open the document using either openFile, openAsset, or openData.
// For Web, file name can be relative path from index.html or any arbitrary URL
// but affected by CORS.
PdfDocument doc = await PdfDocument.openAsset('assets/hello.pdf');
// Get the number of pages in the PDF file
int pageCount = doc!.pageCount;
// The first page is 1
PdfPage page = await doc!.getPage(1);
// For the render function's return, see explanation below
PdfPageImage pageImage = await page.render();
// Now, you can access pageImage!.pixels for raw RGBA data
// ...
// Generating dart:ui.Image cache for later use by imageIfAvailable
await pageImage.createImageIfNotAvailable();
// PDFDocument must be disposed as soon as possible.
doc!.dispose();
And then, you can use PdfPageImage to get the actual RGBA image in dart:ui.Image.
To embed the image in the widget tree, you can use RawImage:
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.all(10.0),
color: Colors.grey,
child: Center(
// before using imageIfAvailable, you should call createImageIfNotAvailable
child: RawImage(image: pageImage.imageIfAvailable, fit: BoxFit.contain))
)
);
}
If you just building widget tree, you had better use faster and efficient PdfPageImageTexture.
On PdfDocument class, there are three functions to open PDF from a real file, an asset file, or a memory data.
// from an asset file
PdfDocument docFromAsset = await PdfDocument.openAsset('assets/hello.pdf');
// from a file
// For Web, file name can be relative path from index.html or any arbitrary URL
// but affected by CORS.
PdfDocument docFromFile = await PdfDocument.openFile('/somewhere/in/real/file/system/file.pdf');
// from PDF memory image on Uint8List
PdfDocument docFromData = await PdfDocument.openData(data);
PdfDocument class overview:
class PdfDocument {
/// File path, `asset:[ASSET_PATH]` or `memory:` depending on the content opened.
final String sourceName;
/// Number of pages in the PDF document.
final int pageCount;
/// PDF major version.
final int verMajor;
/// PDF minor version.
final int verMinor;
/// Determine whether the PDF file is encrypted or not.
final bool isEncrypted;
/// Determine whether the PDF file allows copying of the contents.
final bool allowsCopying;
/// Determine whether the PDF file allows printing of the pages.
final bool allowsPrinting;
// Get a page by page number (page number starts at 1)
Future<PdfPage> getPage(int pageNumber);
// Dispose the instance.
Future<void> dispose();
}
PdfPage class overview:
class PdfPage {
final PdfDocument document; // For internal purpose
final int pageNumber; // Page number (page number starts at 1)
final double width; // Page width in points; pixel size on 72-dpi
final double height; // Page height in points; pixel size on 72-dpi
// render sub-region of the PDF page.
Future<PdfPageImage> render({
int x = 0,
int y = 0,
int? width,
int? height,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
bool allowAntialiasingIOS = false
});
}
render function extracts a sub-region (x,y)
- (x + width, y + height)
from scaled fullWidth
x fullHeight
PDF page image. All the coordinates are in pixels.
The following fragment renders the page at 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 class overview:
class PdfPageImage {
/// Page number. The first page is 1.
final int pageNumber;
/// Left X coordinate of the rendered area in pixels.
final int x;
/// Top Y coordinate of the rendered area in pixels.
final int y;
/// Width of the rendered area in pixels.
final int width;
/// Height of the rendered area in pixels.
final int height;
/// Full width of the rendered page image in pixels.
final int fullWidth;
/// Full height of the rendered page image in pixels.
final int fullHeight;
/// PDF page width in points (width in pixels at 72 dpi).
final double pageWidth;
/// PDF page height in points (height in pixels at 72 dpi).
final double pageHeight;
/// RGBA pixels in byte array.
final Uint8List pixels;
/// Get [dart:ui.Image] for the object.
Future<Image> createImageIfNotAvailable() async;
/// Get [Image] for the object if available; otherwise null.
/// If you want to ensure that the [Image] is available,
/// call [createImageIfNotAvailable].
Image? get imageIfAvailable;
}
createImageIfNotAvailable generates image cache in dart:ui.Image and imageIfAvailable returns the cached image if available.
If you just need RGBA byte array, you can use pixels for that purpose. The pixel at (x,y)
is on pixels[(x+y*width)*4]
. Anyway, it's highly discouraged to modify the contents directly though it would work correctly.
PdfPageImageTexture is to utilize Flutter's Texture class to realize faster and resource-saving rendering comparing to PdfPageImage/RawImage combination.
class PdfPageImageTexture {
final PdfDocument pdfDocument;
final int pageNumber;
final int texId;
int? get texWidth;
int? get texHeight;
bool get hasUpdatedTexture;
PdfPageImageTexture({required this.pdfDocument, required this.pageNumber, required this.texId});
/// Create a new Flutter [Texture]. The object should be released by calling [dispose] method after use it.
static Future<PdfPageImageTexture> create({required PdfDocument pdfDocument, required int pageNumber});
/// Release the object.
Future<void> dispose();
/// Update texture's sub-rectangle ([destX],[destY],[width],[height]) with the sub-rectangle ([srcX],[srcY],[width],[height]) of the PDF page scaled to [fullWidth] x [fullHeight] size.
/// If [backgroundFill] is true, the sub-rectangle is filled with white before rendering the page content.
/// The method can also resize the texture if you specify [texWidth] and [texHeight].
Future<bool> updateRect({
int destX = 0,
int destY = 0,
int? width,
int? height,
int srcX = 0,
int srcY = 0,
int? texWidth,
int? texHeight,
double? fullWidth,
double? fullHeight,
bool backgroundFill = true,
});
}