David PHAM-VAN

Add PdfPreview Widget

1 # Changelog 1 # Changelog
2 2
  3 +## 3.4.0
  4 +
  5 +- Add PdfPreview Widget
  6 +
3 ## 3.3.1 7 ## 3.3.1
4 8
5 - Remove width and height parameters from wrapWidget helper 9 - Remove width and height parameters from wrapWidget helper
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 export 'src/asset_utils.dart'; 17 export 'src/asset_utils.dart';
18 export 'src/callback.dart'; 18 export 'src/callback.dart';
  19 +export 'src/pdf_preview.dart';
19 export 'src/printer.dart'; 20 export 'src/printer.dart';
20 export 'src/printing.dart'; 21 export 'src/printing.dart';
21 export 'src/printing_info.dart'; 22 export 'src/printing_info.dart';
  1 +import 'dart:math';
  2 +import 'dart:typed_data';
  3 +
  4 +import 'package:flutter/material.dart';
  5 +import 'package:pdf/pdf.dart';
  6 +import 'package:pdf/widgets.dart' as pw;
  7 +
  8 +import 'callback.dart';
  9 +import 'printing.dart';
  10 +import 'printing_info.dart';
  11 +import 'raster.dart';
  12 +
  13 +class PdfPreview extends StatefulWidget {
  14 + const PdfPreview({
  15 + Key key,
  16 + @required this.build,
  17 + this.initialPageFormat,
  18 + this.allowPrinting = true,
  19 + this.allowSharing = true,
  20 + this.canChangePageFormat = true,
  21 + this.actions,
  22 + this.pageFormats,
  23 + this.onError,
  24 + this.onPrinted,
  25 + this.onShared,
  26 + }) : super(key: key);
  27 +
  28 + final LayoutCallback build;
  29 +
  30 + final PdfPageFormat initialPageFormat;
  31 +
  32 + final bool allowPrinting;
  33 +
  34 + final bool allowSharing;
  35 +
  36 + final bool canChangePageFormat;
  37 +
  38 + final List<PdfPreviewAction> actions;
  39 +
  40 + final Map<String, PdfPageFormat> pageFormats;
  41 +
  42 + final Widget Function(BuildContext context) onError;
  43 +
  44 + final void Function(BuildContext context) onPrinted;
  45 +
  46 + final void Function(BuildContext context) onShared;
  47 +
  48 + @override
  49 + _PdfPreviewState createState() => _PdfPreviewState();
  50 +}
  51 +
  52 +class _PdfPreviewState extends State<PdfPreview> {
  53 + final GlobalKey<State<StatefulWidget>> shareWidget = GlobalKey();
  54 + final GlobalKey<State<StatefulWidget>> listView = GlobalKey();
  55 +
  56 + final List<_PdfPreviewPage> pages = <_PdfPreviewPage>[];
  57 +
  58 + PdfPageFormat pageFormat;
  59 +
  60 + PrintingInfo info = PrintingInfo.unavailable;
  61 + bool infoLoaded = false;
  62 +
  63 + double dpi = 10;
  64 +
  65 + dynamic error;
  66 +
  67 + static const Map<String, PdfPageFormat> defaultPageFormats =
  68 + <String, PdfPageFormat>{
  69 + 'A4': PdfPageFormat.a4,
  70 + 'Letter': PdfPageFormat.letter,
  71 + };
  72 +
  73 + Future<void> _raster() async {
  74 + Uint8List _doc;
  75 +
  76 + if (!info.canRaster) {
  77 + return;
  78 + }
  79 +
  80 + try {
  81 + _doc = await widget.build(pageFormat);
  82 + } catch (e) {
  83 + error = e;
  84 + return;
  85 + }
  86 +
  87 + if (error != null) {
  88 + setState(() {
  89 + error = null;
  90 + });
  91 + }
  92 +
  93 + int pageNum = 0;
  94 + await for (final PdfRaster page in Printing.raster(_doc, dpi: dpi)) {
  95 + setState(() {
  96 + if (pages.length <= pageNum) {
  97 + pages.add(_PdfPreviewPage(page: page));
  98 + } else {
  99 + pages[pageNum] = _PdfPreviewPage(page: page);
  100 + }
  101 + });
  102 +
  103 + pageNum++;
  104 + }
  105 +
  106 + pages.removeRange(pageNum, pages.length);
  107 + }
  108 +
  109 + @override
  110 + void initState() {
  111 + final Locale locale =
  112 + WidgetsBinding.instance.window.locale ?? const Locale('en', 'US');
  113 + final String cc = locale.countryCode;
  114 + if (cc == 'US' || cc == 'CA' || cc == 'MX') {
  115 + pageFormat = widget.initialPageFormat ?? PdfPageFormat.letter;
  116 + } else {
  117 + pageFormat = widget.initialPageFormat ?? PdfPageFormat.a4;
  118 + }
  119 +
  120 + super.initState();
  121 + }
  122 +
  123 + @override
  124 + void reassemble() {
  125 + _raster();
  126 + super.reassemble();
  127 + }
  128 +
  129 + @override
  130 + void didUpdateWidget(covariant PdfPreview oldWidget) {
  131 + if (oldWidget.build != widget.build) {
  132 + pages.clear();
  133 + _raster();
  134 + }
  135 + super.didUpdateWidget(oldWidget);
  136 + }
  137 +
  138 + @override
  139 + void didChangeDependencies() {
  140 + if (!infoLoaded) {
  141 + Printing.info().then((PrintingInfo _info) {
  142 + setState(() {
  143 + infoLoaded = true;
  144 + info = _info;
  145 + _raster();
  146 + });
  147 + });
  148 + }
  149 +
  150 + final MediaQueryData mq = MediaQuery.of(context);
  151 + dpi = (mq.size.width - 16) *
  152 + min(mq.devicePixelRatio, 2) /
  153 + pageFormat.width *
  154 + 72;
  155 +
  156 + _raster();
  157 + super.didChangeDependencies();
  158 + }
  159 +
  160 + Widget _showError() {
  161 + if (widget.onError != null) {
  162 + return widget.onError(context);
  163 + }
  164 +
  165 + return const Center(
  166 + child: Text(
  167 + 'Unable to display the document',
  168 + style: TextStyle(
  169 + fontSize: 20,
  170 + ),
  171 + ),
  172 + );
  173 + }
  174 +
  175 + Widget _createPreview() {
  176 + if (error != null) {
  177 + Widget content = _showError();
  178 + assert(() {
  179 + content = ErrorWidget.withDetails(
  180 + message: error.toString(),
  181 + );
  182 + return true;
  183 + }());
  184 + return content;
  185 + }
  186 +
  187 + if (!info.canRaster) {
  188 + return _showError();
  189 + }
  190 +
  191 + if (pages.isEmpty) {
  192 + return const Center(child: CircularProgressIndicator());
  193 + }
  194 +
  195 + return Scrollbar(
  196 + child: ListView.builder(
  197 + itemCount: pages.length,
  198 + itemBuilder: (BuildContext context, int index) => pages[index],
  199 + ),
  200 + );
  201 + }
  202 +
  203 + @override
  204 + Widget build(BuildContext context) {
  205 + final ThemeData theme = Theme.of(context);
  206 +
  207 + final Widget scrollView = Container(
  208 + width: double.infinity,
  209 + decoration: BoxDecoration(
  210 + gradient: LinearGradient(
  211 + colors: <Color>[Colors.grey.shade400, Colors.grey.shade200],
  212 + begin: Alignment.topCenter,
  213 + end: Alignment.bottomCenter,
  214 + ),
  215 + ),
  216 + child: _createPreview(),
  217 + );
  218 +
  219 + final List<Widget> actions = <Widget>[];
  220 +
  221 + if (widget.allowPrinting && info.canPrint) {
  222 + actions.add(
  223 + IconButton(
  224 + icon: const Icon(Icons.print),
  225 + color: theme.accentIconTheme.color,
  226 + onPressed: _print,
  227 + ),
  228 + );
  229 + }
  230 +
  231 + if (widget.allowSharing && info.canShare) {
  232 + actions.add(
  233 + IconButton(
  234 + key: shareWidget,
  235 + icon: const Icon(Icons.share),
  236 + color: theme.accentIconTheme.color,
  237 + onPressed: _share,
  238 + ),
  239 + );
  240 + }
  241 +
  242 + if (widget.canChangePageFormat) {
  243 + final Map<String, PdfPageFormat> _pageFormats =
  244 + widget.pageFormats ?? defaultPageFormats;
  245 + final List<String> keys = _pageFormats.keys.toList();
  246 + actions.add(
  247 + DropdownButton<PdfPageFormat>(
  248 + style: theme.accentTextTheme.button,
  249 + dropdownColor: Colors.grey.shade700,
  250 + icon: Icon(
  251 + Icons.arrow_drop_down,
  252 + color: theme.accentIconTheme.color,
  253 + ),
  254 + value: pageFormat,
  255 + items: List<DropdownMenuItem<PdfPageFormat>>.generate(
  256 + _pageFormats.length,
  257 + (int index) {
  258 + final String key = keys[index];
  259 + final PdfPageFormat val = _pageFormats[key];
  260 + return DropdownMenuItem<PdfPageFormat>(
  261 + child: Text(key),
  262 + value: val,
  263 + );
  264 + },
  265 + ),
  266 + onChanged: (PdfPageFormat _pageFormat) {
  267 + setState(() {
  268 + pageFormat = _pageFormat;
  269 + _raster();
  270 + });
  271 + },
  272 + ),
  273 + );
  274 + }
  275 +
  276 + if (widget.actions != null) {
  277 + for (final PdfPreviewAction action in widget.actions) {
  278 + actions.add(
  279 + IconButton(
  280 + icon: action.icon,
  281 + color: theme.accentIconTheme.color,
  282 + onPressed: action.onPressed == null
  283 + ? null
  284 + : () => action.onPressed(
  285 + context,
  286 + widget.build,
  287 + pageFormat,
  288 + ),
  289 + ),
  290 + );
  291 + }
  292 + }
  293 +
  294 + assert(() {
  295 + if (actions.isNotEmpty) {
  296 + actions.add(
  297 + Switch(
  298 + activeColor: Colors.red,
  299 + value: pw.Document.debug,
  300 + onChanged: (bool value) {
  301 + setState(
  302 + () {
  303 + pw.Document.debug = value;
  304 + _raster();
  305 + },
  306 + );
  307 + },
  308 + ),
  309 + );
  310 + }
  311 +
  312 + return true;
  313 + }());
  314 +
  315 + return Column(
  316 + mainAxisAlignment: MainAxisAlignment.center,
  317 + children: <Widget>[
  318 + Expanded(child: scrollView),
  319 + if (actions.isNotEmpty)
  320 + Material(
  321 + elevation: 4,
  322 + color: theme.primaryColor,
  323 + child: Row(
  324 + mainAxisAlignment: MainAxisAlignment.spaceAround,
  325 + children: actions,
  326 + ),
  327 + )
  328 + ],
  329 + );
  330 + }
  331 +
  332 + Future<void> _print() async {
  333 + final bool result = await Printing.layoutPdf(onLayout: widget.build);
  334 +
  335 + if (result && widget.onPrinted != null) {
  336 + widget.onPrinted(context);
  337 + }
  338 + }
  339 +
  340 + Future<void> _share() async {
  341 + // Calculate the widget center for iPad sharing popup position
  342 + final RenderBox referenceBox =
  343 + shareWidget.currentContext.findRenderObject();
  344 + final Offset topLeft =
  345 + referenceBox.localToGlobal(referenceBox.paintBounds.topLeft);
  346 + final Offset bottomRight =
  347 + referenceBox.localToGlobal(referenceBox.paintBounds.bottomRight);
  348 + final Rect bounds = Rect.fromPoints(topLeft, bottomRight);
  349 +
  350 + final Uint8List bytes = await widget.build(pageFormat);
  351 + final bool result = await Printing.sharePdf(bytes: bytes, bounds: bounds);
  352 +
  353 + if (result && widget.onShared != null) {
  354 + widget.onShared(context);
  355 + }
  356 + }
  357 +}
  358 +
  359 +class _PdfPreviewPage extends StatelessWidget {
  360 + const _PdfPreviewPage({
  361 + Key key,
  362 + this.page,
  363 + }) : super(key: key);
  364 +
  365 + final PdfRaster page;
  366 +
  367 + @override
  368 + Widget build(BuildContext context) {
  369 + final PdfRasterImage im = PdfRasterImage(page);
  370 +
  371 + return Container(
  372 + margin: const EdgeInsets.only(
  373 + left: 8,
  374 + top: 8,
  375 + right: 8,
  376 + bottom: 12,
  377 + ),
  378 + decoration: const BoxDecoration(
  379 + color: Colors.white,
  380 + boxShadow: <BoxShadow>[
  381 + BoxShadow(
  382 + offset: Offset(0, 3),
  383 + blurRadius: 5,
  384 + color: Color(0xFF000000),
  385 + ),
  386 + ],
  387 + ),
  388 + child: AspectRatio(
  389 + aspectRatio: page.width / page.height,
  390 + child: Image(
  391 + image: im,
  392 + fit: BoxFit.cover,
  393 + ),
  394 + ),
  395 + );
  396 + }
  397 +}
  398 +
  399 +typedef OnPdfPreviewActionPressed = void Function(
  400 + BuildContext context,
  401 + LayoutCallback build,
  402 + PdfPageFormat pageFormat,
  403 +);
  404 +
  405 +class PdfPreviewAction {
  406 + const PdfPreviewAction({
  407 + @required this.icon,
  408 + @required this.onPressed,
  409 + }) : assert(icon != null);
  410 +
  411 + final Icon icon;
  412 + final OnPdfPreviewActionPressed onPressed;
  413 +}
@@ -4,7 +4,7 @@ description: Plugin that allows Flutter apps to generate and print documents to @@ -4,7 +4,7 @@ description: Plugin that allows Flutter apps to generate and print documents to
4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/printing 4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/printing
5 repository: https://github.com/DavBfr/dart_pdf 5 repository: https://github.com/DavBfr/dart_pdf
6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues 6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues
7 -version: 3.3.1 7 +version: 3.4.0
8 8
9 environment: 9 environment:
10 sdk: ">=2.3.0 <3.0.0" 10 sdk: ">=2.3.0 <3.0.0"