Showing
4 changed files
with
419 additions
and
1 deletions
@@ -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'; |
printing/lib/src/pdf_preview.dart
0 → 100644
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" |
-
Please register or login to post a comment