David PHAM-VAN

Add html to pdf platform conversion

@@ -28,6 +28,7 @@ printing/android/local.properties @@ -28,6 +28,7 @@ printing/android/local.properties
28 printing/*.log 28 printing/*.log
29 printing/ios/Flutter 29 printing/ios/Flutter
30 printing/ios/Runner 30 printing/ios/Runner
  31 +printing/android/.gradle
31 32
32 .pana 33 .pana
33 pedantic_analyis_options_*.yaml 34 pedantic_analyis_options_*.yaml
  1 +## 2.1.0
  2 +
  3 +* Add html to pdf platform conversion
  4 +
1 ## 2.0.4 5 ## 2.0.4
2 6
3 * Update Readme 7 * Update Readme
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package android.print;
  18 +
  19 +import android.app.Activity;
  20 +import android.os.CancellationSignal;
  21 +import android.os.ParcelFileDescriptor;
  22 +
  23 +import java.io.File;
  24 +import java.io.FileInputStream;
  25 +import java.io.FileNotFoundException;
  26 +import java.io.IOException;
  27 +import java.io.InputStream;
  28 +
  29 +public class PdfConvert {
  30 + public static void print(final Activity activity, final PrintDocumentAdapter adapter,
  31 + final PrintAttributes attributes, final Result result) {
  32 + adapter.onLayout(null, attributes, null, new PrintDocumentAdapter.LayoutResultCallback() {
  33 + @Override
  34 + public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
  35 + File outputDir = activity.getCacheDir();
  36 + File outputFile = null;
  37 + try {
  38 + outputFile = File.createTempFile("printing", "pdf", outputDir);
  39 + } catch (IOException e) {
  40 + outputFile.delete();
  41 + result.onError(e.getMessage());
  42 + return;
  43 + }
  44 +
  45 + try {
  46 + final File finalOutputFile = outputFile;
  47 + adapter.onWrite(new PageRange[] {PageRange.ALL_PAGES},
  48 + ParcelFileDescriptor.open(
  49 + outputFile, ParcelFileDescriptor.MODE_READ_WRITE),
  50 + new CancellationSignal(),
  51 + new PrintDocumentAdapter.WriteResultCallback() {
  52 + @Override
  53 + public void onWriteFinished(PageRange[] pages) {
  54 + super.onWriteFinished(pages);
  55 +
  56 + if (pages.length == 0) {
  57 + finalOutputFile.delete();
  58 + result.onError("No page created");
  59 + }
  60 +
  61 + result.onSuccess(finalOutputFile);
  62 + finalOutputFile.delete();
  63 + }
  64 + });
  65 + } catch (FileNotFoundException e) {
  66 + outputFile.delete();
  67 + result.onError(e.getMessage());
  68 + }
  69 + }
  70 + }, null);
  71 + }
  72 +
  73 + public static byte[] readFile(File file) throws IOException {
  74 + byte[] buffer = new byte[(int) file.length()];
  75 + InputStream ios = null;
  76 + try {
  77 + ios = new FileInputStream(file);
  78 + if (ios.read(buffer) == -1) {
  79 + throw new IOException("EOF reached while trying to read the whole file");
  80 + }
  81 + } finally {
  82 + try {
  83 + if (ios != null) ios.close();
  84 + } catch (IOException e) {
  85 + }
  86 + }
  87 + return buffer;
  88 + }
  89 +
  90 + public interface Result {
  91 + void onSuccess(File file);
  92 +
  93 + void onError(String message);
  94 + }
  95 +}
@@ -16,22 +16,24 @@ @@ -16,22 +16,24 @@
16 16
17 package net.nfet.flutter.printing; 17 package net.nfet.flutter.printing;
18 18
19 -import static java.lang.StrictMath.max;  
20 -  
21 import android.app.Activity; 19 import android.app.Activity;
22 import android.content.Context; 20 import android.content.Context;
23 import android.content.Intent; 21 import android.content.Intent;
24 import android.net.Uri; 22 import android.net.Uri;
  23 +import android.os.Build;
25 import android.os.Bundle; 24 import android.os.Bundle;
26 import android.os.CancellationSignal; 25 import android.os.CancellationSignal;
27 import android.os.Environment; 26 import android.os.Environment;
28 import android.os.ParcelFileDescriptor; 27 import android.os.ParcelFileDescriptor;
29 import android.print.PageRange; 28 import android.print.PageRange;
  29 +import android.print.PdfConvert;
30 import android.print.PrintAttributes; 30 import android.print.PrintAttributes;
31 import android.print.PrintDocumentAdapter; 31 import android.print.PrintDocumentAdapter;
32 import android.print.PrintDocumentInfo; 32 import android.print.PrintDocumentInfo;
33 import android.print.PrintManager; 33 import android.print.PrintManager;
34 import android.print.pdf.PrintedPdfDocument; 34 import android.print.pdf.PrintedPdfDocument;
  35 +import android.webkit.WebView;
  36 +import android.webkit.WebViewClient;
35 import androidx.core.content.FileProvider; 37 import androidx.core.content.FileProvider;
36 38
37 import java.io.File; 39 import java.io.File;
@@ -155,6 +157,28 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa @@ -155,6 +157,28 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
155 sharePdf((byte[]) call.argument("doc"), (String) call.argument("name")); 157 sharePdf((byte[]) call.argument("doc"), (String) call.argument("name"));
156 result.success(0); 158 result.success(0);
157 break; 159 break;
  160 + case "convertHtml":
  161 + double width = (double) call.argument("width");
  162 + double height = (double) call.argument("height");
  163 + double marginLeft = (double) call.argument("marginLeft");
  164 + double marginTop = (double) call.argument("marginTop");
  165 + double marginRight = (double) call.argument("marginRight");
  166 + double marginBottom = (double) call.argument("marginBottom");
  167 +
  168 + PrintAttributes.Margins margins =
  169 + new PrintAttributes.Margins(Double.valueOf(marginLeft * 1000.0).intValue(),
  170 + Double.valueOf(marginTop * 1000.0 / 72.0).intValue(),
  171 + Double.valueOf(marginRight * 1000.0 / 72.0).intValue(),
  172 + Double.valueOf(marginBottom * 1000.0 / 72.0).intValue());
  173 +
  174 + PrintAttributes.MediaSize size = new PrintAttributes.MediaSize("flutter_printing",
  175 + "Provided size", Double.valueOf(width * 1000.0 / 72.0).intValue(),
  176 + Double.valueOf(height * 1000.0 / 72.0).intValue());
  177 +
  178 + convertHtml((String) call.argument("html"), size, margins,
  179 + (String) call.argument("baseUrl"));
  180 + result.success(0);
  181 + break;
158 default: 182 default:
159 result.notImplemented(); 183 result.notImplemented();
160 break; 184 break;
@@ -192,4 +216,49 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa @@ -192,4 +216,49 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
192 e.printStackTrace(); 216 e.printStackTrace();
193 } 217 }
194 } 218 }
  219 +
  220 + private void convertHtml(String data, final PrintAttributes.MediaSize size,
  221 + final PrintAttributes.Margins margins, String baseUrl) {
  222 + final WebView webView = new WebView(activity.getApplicationContext());
  223 +
  224 + webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null);
  225 +
  226 + webView.setWebViewClient(new WebViewClient() {
  227 + @Override
  228 + public void onPageFinished(WebView view, String url) {
  229 + super.onPageFinished(view, url);
  230 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  231 + PrintAttributes attributes =
  232 + new PrintAttributes.Builder()
  233 + .setMediaSize(size)
  234 + .setResolution(
  235 + new PrintAttributes.Resolution("pdf", "pdf", 600, 600))
  236 + .setMinMargins(margins)
  237 + .build();
  238 +
  239 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  240 + final PrintDocumentAdapter adapter =
  241 + webView.createPrintDocumentAdapter("printing");
  242 +
  243 + PdfConvert.print(activity, adapter, attributes, new PdfConvert.Result() {
  244 + @Override
  245 + public void onSuccess(File file) {
  246 + try {
  247 + byte[] fileContent = PdfConvert.readFile(file);
  248 + channel.invokeMethod("onHtmlRendered", fileContent);
  249 + } catch (IOException e) {
  250 + onError(e.getMessage());
  251 + }
  252 + }
  253 +
  254 + @Override
  255 + public void onError(String message) {
  256 + channel.invokeMethod("onHtmlError", message);
  257 + }
  258 + });
  259 + }
  260 + }
  261 + }
  262 + });
  263 + }
195 } 264 }
  1 +<!DOCTYPE html>
  2 +<html>
  3 +
  4 +<head>
  5 + <style>
  6 + * {
  7 + font-family: Arial, Helvetica, sans-serif;
  8 + }
  9 +
  10 + table,
  11 + th,
  12 + td {
  13 + border: 1px solid black;
  14 + border-collapse: collapse;
  15 + }
  16 +
  17 + th,
  18 + td {
  19 + padding: 5px;
  20 + }
  21 + </style>
  22 +</head>
  23 +
  24 +<body>
  25 + <h2>Html conversion</h2>
  26 +
  27 + <caption>Sample HTML Table</caption>
  28 + <table>
  29 + <tr>
  30 + <th>Company</th>
  31 + <th>Contact</th>
  32 + <th>Country</th>
  33 + </tr>
  34 + <tr>
  35 + <td>Alfreds Futterkiste</td>
  36 + <td>Maria Anders</td>
  37 + <td>Germany</td>
  38 + </tr>
  39 + <tr>
  40 + <td>Centro comercial Moctezuma</td>
  41 + <td>Francisco Chang</td>
  42 + <td>Mexico</td>
  43 + </tr>
  44 + <tr>
  45 + <td>Ernst Handel</td>
  46 + <td>Roland Mendel</td>
  47 + <td>Austria</td>
  48 + </tr>
  49 + <tr>
  50 + <td>Island Trading</td>
  51 + <td>Helen Bennett</td>
  52 + <td>UK</td>
  53 + </tr>
  54 + <tr>
  55 + <td>Laughing Bacchus Winecellars</td>
  56 + <td>Yoshi Tannamuri</td>
  57 + <td>Canada</td>
  58 + </tr>
  59 + <tr>
  60 + <td>Magazzini Alimentari Riuniti</td>
  61 + <td>Giovanni Rovelli</td>
  62 + <td>Italy</td>
  63 + </tr>
  64 + </table>
  65 +
  66 + <p>HTML links are defined with the a tag:</p>
  67 +
  68 + <a href="https://github.com/DavBfr/dart_pdf">This is a link</a>
  69 +
  70 + <h2>An Unordered HTML List</h2>
  71 +
  72 + <ul>
  73 + <li>Coffee</li>
  74 + <li>Tea</li>
  75 + <li>Milk</li>
  76 + </ul>
  77 +
  78 + <h2>An Ordered HTML List</h2>
  79 +
  80 + <ol>
  81 + <li>Coffee</li>
  82 + <li>Tea</li>
  83 + <li>Milk</li>
  84 + </ol>
  85 +
  86 +</body>
  87 +
  88 +</html>
@@ -5,6 +5,7 @@ import 'dart:ui' as ui; @@ -5,6 +5,7 @@ import 'dart:ui' as ui;
5 5
6 import 'package:flutter/material.dart'; 6 import 'package:flutter/material.dart';
7 import 'package:flutter/rendering.dart'; 7 import 'package:flutter/rendering.dart';
  8 +import 'package:flutter/services.dart';
8 import 'package:path_provider/path_provider.dart'; 9 import 'package:path_provider/path_provider.dart';
9 10
10 import 'package:pdf/pdf.dart'; 11 import 'package:pdf/pdf.dart';
@@ -94,6 +95,14 @@ class MyAppState extends State<MyApp> { @@ -94,6 +95,14 @@ class MyAppState extends State<MyApp> {
94 }); 95 });
95 } 96 }
96 97
  98 + Future<void> _printHtml() async {
  99 + print('Print html ...');
  100 + await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
  101 + final String html = await rootBundle.loadString('assets/example.html');
  102 + return await Printing.convertHtml(format: format, html: html);
  103 + });
  104 + }
  105 +
97 @override 106 @override
98 Widget build(BuildContext context) { 107 Widget build(BuildContext context) {
99 return RepaintBoundary( 108 return RepaintBoundary(
@@ -117,6 +126,8 @@ class MyAppState extends State<MyApp> { @@ -117,6 +126,8 @@ class MyAppState extends State<MyApp> {
117 onPressed: _printScreen), 126 onPressed: _printScreen),
118 RaisedButton( 127 RaisedButton(
119 child: const Text('Save to file'), onPressed: _saveAsFile), 128 child: const Text('Save to file'), onPressed: _saveAsFile),
  129 + RaisedButton(
  130 + child: const Text('Print Html'), onPressed: _printHtml),
120 ], 131 ],
121 ), 132 ),
122 ), 133 ),
@@ -26,3 +26,5 @@ dependency_overrides: @@ -26,3 +26,5 @@ dependency_overrides:
26 26
27 flutter: 27 flutter:
28 uses-material-design: true 28 uses-material-design: true
  29 + assets:
  30 + - assets/
@@ -16,10 +16,12 @@ @@ -16,10 +16,12 @@
16 16
17 import Flutter 17 import Flutter
18 import UIKit 18 import UIKit
  19 +import WebKit
19 20
20 public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControllerDelegate { 21 public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControllerDelegate {
21 private var channel: FlutterMethodChannel? 22 private var channel: FlutterMethodChannel?
22 private var renderer: PdfPrintPageRenderer? 23 private var renderer: PdfPrintPageRenderer?
  24 + private var urlObservation: NSKeyValueObservation?
23 25
24 init(_ channel: FlutterMethodChannel?) { 26 init(_ channel: FlutterMethodChannel?) {
25 super.init() 27 super.init()
@@ -52,6 +54,30 @@ public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionCon @@ -52,6 +54,30 @@ public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionCon
52 ) 54 )
53 } 55 }
54 result(NSNumber(value: 1)) 56 result(NSNumber(value: 1))
  57 + } else if call.method == "convertHtml" {
  58 + let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0)
  59 + let height = CGFloat((args["height"] as? NSNumber)?.floatValue ?? 0.0)
  60 + let marginLeft = CGFloat((args["marginLeft"] as? NSNumber)?.floatValue ?? 0.0)
  61 + let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0)
  62 + let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0)
  63 + let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0)
  64 + convertHtml(
  65 + (args["html"] as? String)!,
  66 + withPageSize: CGRect(
  67 + x: 0.0,
  68 + y: 0.0,
  69 + width: width,
  70 + height: height
  71 + ),
  72 + andMargin: CGRect(
  73 + x: marginLeft,
  74 + y: marginTop,
  75 + width: width - marginRight - marginLeft,
  76 + height: height - marginBottom - marginTop
  77 + ),
  78 + andBaseUrl: args["baseUrl"] as? String == nil ? nil : URL(string: (args["baseUrl"] as? String)!)
  79 + )
  80 + result(NSNumber(value: 1))
55 } else { 81 } else {
56 result(FlutterMethodNotImplemented) 82 result(FlutterMethodNotImplemented)
57 } 83 }
@@ -118,4 +144,56 @@ public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionCon @@ -118,4 +144,56 @@ public class SwiftPrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionCon
118 } 144 }
119 UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true) 145 UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)
120 } 146 }
  147 +
  148 + func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {
  149 + let viewController = UIApplication.shared.delegate?.window?!.rootViewController
  150 + let wkWebView = WKWebView(frame: viewController!.view.bounds)
  151 + wkWebView.isHidden = true
  152 + wkWebView.tag = 100
  153 + viewController?.view.addSubview(wkWebView)
  154 + wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)
  155 +
  156 + urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in
  157 + // this is workaround for issue with loading local images
  158 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  159 + // assign the print formatter to the print page renderer
  160 + let renderer = UIPrintPageRenderer()
  161 + renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)
  162 +
  163 + // assign paperRect and printableRect values
  164 + renderer.setValue(rect, forKey: "paperRect")
  165 + renderer.setValue(margin, forKey: "printableRect")
  166 +
  167 + // create pdf context and draw each page
  168 + let pdfData = NSMutableData()
  169 + UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)
  170 +
  171 + for i in 0 ..< renderer.numberOfPages {
  172 + UIGraphicsBeginPDFPage()
  173 + renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
  174 + }
  175 +
  176 + UIGraphicsEndPDFContext()
  177 +
  178 + if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {
  179 + viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated
  180 +
  181 + // clear WKWebView cache
  182 + if #available(iOS 9.0, *) {
  183 + WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
  184 + records.forEach { record in
  185 + WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
  186 + }
  187 + }
  188 + }
  189 + }
  190 +
  191 + // dispose urlObservation
  192 + self.urlObservation = nil
  193 +
  194 + let data = FlutterStandardTypedData(bytes: pdfData as Data)
  195 + self.channel?.invokeMethod("onHtmlRendered", arguments: data)
  196 + }
  197 + })
  198 + }
121 } 199 }
@@ -21,6 +21,7 @@ typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format); @@ -21,6 +21,7 @@ typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format);
21 mixin Printing { 21 mixin Printing {
22 static const MethodChannel _channel = MethodChannel('printing'); 22 static const MethodChannel _channel = MethodChannel('printing');
23 static LayoutCallback _onLayout; 23 static LayoutCallback _onLayout;
  24 + static Completer<List<int>> _onHtmlRendered;
24 25
25 static Future<void> _handleMethod(MethodCall call) async { 26 static Future<void> _handleMethod(MethodCall call) async {
26 switch (call.method) { 27 switch (call.method) {
@@ -37,6 +38,12 @@ mixin Printing { @@ -37,6 +38,12 @@ mixin Printing {
37 'doc': Uint8List.fromList(bytes), 38 'doc': Uint8List.fromList(bytes),
38 }; 39 };
39 return await _channel.invokeMethod('writePdf', params); 40 return await _channel.invokeMethod('writePdf', params);
  41 + case 'onHtmlRendered':
  42 + _onHtmlRendered.complete(call.arguments);
  43 + break;
  44 + case 'onHtmlError':
  45 + _onHtmlRendered.completeError(call.arguments);
  46 + break;
40 } 47 }
41 } 48 }
42 49
@@ -89,4 +96,25 @@ mixin Printing { @@ -89,4 +96,25 @@ mixin Printing {
89 }; 96 };
90 return await _channel.invokeMethod('sharePdf', params); 97 return await _channel.invokeMethod('sharePdf', params);
91 } 98 }
  99 +
  100 + static Future<List<int>> convertHtml(
  101 + {@required String html,
  102 + String baseUrl,
  103 + PdfPageFormat format = PdfPageFormat.a4}) async {
  104 + final Map<String, dynamic> params = <String, dynamic>{
  105 + 'html': html,
  106 + 'baseUrl': baseUrl,
  107 + 'width': format.width,
  108 + 'height': format.height,
  109 + 'marginLeft': format.marginLeft,
  110 + 'marginTop': format.marginTop,
  111 + 'marginRight': format.marginRight,
  112 + 'marginBottom': format.marginBottom,
  113 + };
  114 +
  115 + _channel.setMethodCallHandler(_handleMethod);
  116 + _onHtmlRendered = Completer<List<int>>();
  117 + await _channel.invokeMethod<void>('convertHtml', params);
  118 + return _onHtmlRendered.future;
  119 + }
92 } 120 }
@@ -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: 2.0.4 7 +version: 2.1.0
8 8
9 environment: 9 environment:
10 sdk: ">=2.1.0 <3.0.0" 10 sdk: ">=2.1.0 <3.0.0"