David PHAM-VAN

Improve native code

@@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
3 ## 2.2.0 3 ## 2.2.0
4 4
5 - Simplify iOS code 5 - Simplify iOS code
  6 +- Improve native code
  7 +- Add Printing.info()
6 8
7 ## 2.1.9 9 ## 2.1.9
8 10
@@ -16,9 +16,10 @@ @@ -16,9 +16,10 @@
16 16
17 package android.print; 17 package android.print;
18 18
19 -import android.app.Activity; 19 +import android.content.Context;
20 import android.os.CancellationSignal; 20 import android.os.CancellationSignal;
21 import android.os.ParcelFileDescriptor; 21 import android.os.ParcelFileDescriptor;
  22 +import android.util.Log;
22 23
23 import java.io.File; 24 import java.io.File;
24 import java.io.FileInputStream; 25 import java.io.FileInputStream;
@@ -27,17 +28,16 @@ import java.io.IOException; @@ -27,17 +28,16 @@ import java.io.IOException;
27 import java.io.InputStream; 28 import java.io.InputStream;
28 29
29 public class PdfConvert { 30 public class PdfConvert {
30 - public static void print(final Activity activity, final PrintDocumentAdapter adapter, 31 + public static void print(final Context context, final PrintDocumentAdapter adapter,
31 final PrintAttributes attributes, final Result result) { 32 final PrintAttributes attributes, final Result result) {
32 adapter.onLayout(null, attributes, null, new PrintDocumentAdapter.LayoutResultCallback() { 33 adapter.onLayout(null, attributes, null, new PrintDocumentAdapter.LayoutResultCallback() {
33 @Override 34 @Override
34 public void onLayoutFinished(PrintDocumentInfo info, boolean changed) { 35 public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
35 - File outputDir = activity.getCacheDir();  
36 - File outputFile = null; 36 + File outputDir = context.getCacheDir();
  37 + File outputFile;
37 try { 38 try {
38 outputFile = File.createTempFile("printing", "pdf", outputDir); 39 outputFile = File.createTempFile("printing", "pdf", outputDir);
39 } catch (IOException e) { 40 } catch (IOException e) {
40 - outputFile.delete();  
41 result.onError(e.getMessage()); 41 result.onError(e.getMessage());
42 return; 42 return;
43 } 43 }
@@ -54,16 +54,22 @@ public class PdfConvert { @@ -54,16 +54,22 @@ public class PdfConvert {
54 super.onWriteFinished(pages); 54 super.onWriteFinished(pages);
55 55
56 if (pages.length == 0) { 56 if (pages.length == 0) {
57 - finalOutputFile.delete(); 57 + if (!finalOutputFile.delete()) {
  58 + Log.e("PDF", "Unable to delete temporary file");
  59 + }
58 result.onError("No page created"); 60 result.onError("No page created");
59 } 61 }
60 62
61 result.onSuccess(finalOutputFile); 63 result.onSuccess(finalOutputFile);
62 - finalOutputFile.delete(); 64 + if (!finalOutputFile.delete()) {
  65 + Log.e("PDF", "Unable to delete temporary file");
  66 + }
63 } 67 }
64 }); 68 });
65 } catch (FileNotFoundException e) { 69 } catch (FileNotFoundException e) {
66 - outputFile.delete(); 70 + if (!outputFile.delete()) {
  71 + Log.e("PDF", "Unable to delete temporary file");
  72 + }
67 result.onError(e.getMessage()); 73 result.onError(e.getMessage());
68 } 74 }
69 } 75 }
@@ -72,17 +78,10 @@ public class PdfConvert { @@ -72,17 +78,10 @@ public class PdfConvert {
72 78
73 public static byte[] readFile(File file) throws IOException { 79 public static byte[] readFile(File file) throws IOException {
74 byte[] buffer = new byte[(int) file.length()]; 80 byte[] buffer = new byte[(int) file.length()];
75 - InputStream ios = null;  
76 - try {  
77 - ios = new FileInputStream(file); 81 + try (InputStream ios = new FileInputStream(file)) {
78 if (ios.read(buffer) == -1) { 82 if (ios.read(buffer) == -1) {
79 throw new IOException("EOF reached while trying to read the whole file"); 83 throw new IOException("EOF reached while trying to read the whole file");
80 } 84 }
81 - } finally {  
82 - try {  
83 - if (ios != null) ios.close();  
84 - } catch (IOException e) {  
85 - }  
86 } 85 }
87 return buffer; 86 return buffer;
88 } 87 }
  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 net.nfet.flutter.printing;
  18 +
  19 +import android.content.Context;
  20 +import android.content.Intent;
  21 +import android.net.Uri;
  22 +import android.os.Build;
  23 +import android.os.Bundle;
  24 +import android.os.CancellationSignal;
  25 +import android.os.Environment;
  26 +import android.os.ParcelFileDescriptor;
  27 +import android.print.PageRange;
  28 +import android.print.PdfConvert;
  29 +import android.print.PrintAttributes;
  30 +import android.print.PrintDocumentAdapter;
  31 +import android.print.PrintDocumentInfo;
  32 +import android.print.PrintJob;
  33 +import android.print.PrintJobInfo;
  34 +import android.print.PrintManager;
  35 +import android.webkit.WebView;
  36 +import android.webkit.WebViewClient;
  37 +
  38 +import androidx.annotation.NonNull;
  39 +import androidx.core.content.FileProvider;
  40 +
  41 +import java.io.File;
  42 +import java.io.FileOutputStream;
  43 +import java.io.IOException;
  44 +import java.io.OutputStream;
  45 +import java.util.HashMap;
  46 +
  47 +/**
  48 + * PrintJob
  49 + */
  50 +public class PrintingJob extends PrintDocumentAdapter {
  51 + private static PrintManager printManager;
  52 + private final Context context;
  53 + private final PrintingPlugin printing;
  54 + private PrintJob printJob;
  55 + private byte[] documentData;
  56 + private String jobName;
  57 + private LayoutResultCallback callback;
  58 + int index;
  59 +
  60 + PrintingJob(Context context, PrintingPlugin printing, int index) {
  61 + this.context = context;
  62 + this.printing = printing;
  63 + this.index = index;
  64 + printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
  65 + }
  66 +
  67 + static HashMap<String, Object> printingInfo() {
  68 + HashMap<String, Object> result = new HashMap<>();
  69 + result.put("directPrint", false);
  70 + result.put("dynamicLayout", true);
  71 + result.put("canPrint", true);
  72 + result.put("canConvertHtml", true);
  73 + result.put("canShare", true);
  74 + return result;
  75 + }
  76 +
  77 + @Override
  78 + public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor parcelFileDescriptor,
  79 + CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback) {
  80 + OutputStream output = null;
  81 + try {
  82 + output = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
  83 + output.write(documentData, 0, documentData.length);
  84 + writeResultCallback.onWriteFinished(new PageRange[] {PageRange.ALL_PAGES});
  85 + } catch (IOException e) {
  86 + e.printStackTrace();
  87 + } finally {
  88 + try {
  89 + if (output != null) {
  90 + output.close();
  91 + }
  92 + } catch (IOException e) {
  93 + e.printStackTrace();
  94 + }
  95 + }
  96 + }
  97 +
  98 + @Override
  99 + public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
  100 + CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
  101 + // Respond to cancellation request
  102 + if (cancellationSignal.isCanceled()) {
  103 + callback.onLayoutCancelled();
  104 + return;
  105 + }
  106 +
  107 + this.callback = callback;
  108 +
  109 + PrintAttributes.MediaSize size = newAttributes.getMediaSize();
  110 + PrintAttributes.Margins margins = newAttributes.getMinMargins();
  111 + assert size != null;
  112 + assert margins != null;
  113 +
  114 + printing.onLayout(this, size.getWidthMils() * 72.0 / 1000.0,
  115 + size.getHeightMils() * 72.0 / 1000.0, margins.getLeftMils() * 72.0 / 1000.0,
  116 + margins.getTopMils() * 72.0 / 1000.0, margins.getRightMils() * 72.0 / 1000.0,
  117 + margins.getBottomMils() * 72.0 / 1000.0);
  118 + }
  119 +
  120 + @Override
  121 + public void onFinish() {
  122 + try {
  123 + while (true) {
  124 + int state = printJob.getInfo().getState();
  125 +
  126 + if (state == PrintJobInfo.STATE_COMPLETED) {
  127 + printing.onCompleted(this, true, "");
  128 + break;
  129 + } else if (state == PrintJobInfo.STATE_CANCELED) {
  130 + printing.onCompleted(this, false, "User canceled");
  131 + break;
  132 + }
  133 +
  134 + Thread.sleep(200);
  135 + }
  136 + } catch (Exception e) {
  137 + printing.onCompleted(this, printJob != null && printJob.isCompleted(), e.getMessage());
  138 + }
  139 +
  140 + printJob = null;
  141 + }
  142 +
  143 + void printPdf(@NonNull String name, Double width, Double height, Double marginLeft,
  144 + Double marginTop, Double marginRight, Double marginBottom) {
  145 + jobName = name;
  146 + printJob = printManager.print(name, this, null);
  147 + }
  148 +
  149 + void cancelJob() {
  150 + if (callback != null) callback.onLayoutCancelled();
  151 + if (printJob != null) printJob.cancel();
  152 + }
  153 +
  154 + static void sharePdf(final Context context, final byte[] data, final String name) {
  155 + assert name != null;
  156 +
  157 + try {
  158 + final File externalFilesDirectory =
  159 + context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
  160 + File shareFile = new File(externalFilesDirectory, name);
  161 +
  162 + FileOutputStream stream = new FileOutputStream(shareFile);
  163 + stream.write(data);
  164 + stream.close();
  165 +
  166 + Uri apkURI = FileProvider.getUriForFile(context,
  167 + context.getApplicationContext().getPackageName() + ".flutter.printing",
  168 + shareFile);
  169 +
  170 + Intent shareIntent = new Intent();
  171 + shareIntent.setAction(Intent.ACTION_SEND);
  172 + shareIntent.setType("application/pdf");
  173 + shareIntent.putExtra(Intent.EXTRA_STREAM, apkURI);
  174 + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  175 + Intent chooserIntent = Intent.createChooser(shareIntent, null);
  176 + context.startActivity(chooserIntent);
  177 + shareFile.deleteOnExit();
  178 + } catch (IOException e) {
  179 + e.printStackTrace();
  180 + }
  181 + }
  182 +
  183 + void convertHtml(final String data, final PrintAttributes.MediaSize size,
  184 + final PrintAttributes.Margins margins, final String baseUrl) {
  185 + final WebView webView = new WebView(context.getApplicationContext());
  186 +
  187 + webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null);
  188 +
  189 + webView.setWebViewClient(new WebViewClient() {
  190 + @Override
  191 + public void onPageFinished(WebView view, String url) {
  192 + super.onPageFinished(view, url);
  193 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  194 + PrintAttributes attributes =
  195 + new PrintAttributes.Builder()
  196 + .setMediaSize(size)
  197 + .setResolution(
  198 + new PrintAttributes.Resolution("pdf", "pdf", 600, 600))
  199 + .setMinMargins(margins)
  200 + .build();
  201 +
  202 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  203 + final PrintDocumentAdapter adapter =
  204 + webView.createPrintDocumentAdapter("printing");
  205 +
  206 + PdfConvert.print(context, adapter, attributes, new PdfConvert.Result() {
  207 + @Override
  208 + public void onSuccess(File file) {
  209 + try {
  210 + byte[] fileContent = PdfConvert.readFile(file);
  211 + printing.onHtmlRendered(PrintingJob.this, fileContent);
  212 + } catch (IOException e) {
  213 + onError(e.getMessage());
  214 + }
  215 + }
  216 +
  217 + @Override
  218 + public void onError(String message) {
  219 + printing.onHtmlError(PrintingJob.this, message);
  220 + }
  221 + });
  222 + }
  223 + }
  224 + }
  225 + });
  226 + }
  227 +
  228 + void setDocument(byte[] data) {
  229 + documentData = data;
  230 +
  231 + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName + ".pdf")
  232 + .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
  233 + .build();
  234 +
  235 + // Content layout reflow is complete
  236 + callback.onLayoutFinished(info, true);
  237 + }
  238 +}
@@ -17,32 +17,10 @@ @@ -17,32 +17,10 @@
17 package net.nfet.flutter.printing; 17 package net.nfet.flutter.printing;
18 18
19 import android.app.Activity; 19 import android.app.Activity;
20 -import android.content.Context;  
21 -import android.content.Intent;  
22 -import android.net.Uri;  
23 -import android.os.Build;  
24 -import android.os.Bundle;  
25 -import android.os.CancellationSignal;  
26 -import android.os.Environment;  
27 -import android.os.ParcelFileDescriptor;  
28 -import android.print.PageRange;  
29 -import android.print.PdfConvert;  
30 import android.print.PrintAttributes; 20 import android.print.PrintAttributes;
31 -import android.print.PrintDocumentAdapter;  
32 -import android.print.PrintDocumentInfo;  
33 -import android.print.PrintJob;  
34 -import android.print.PrintJobInfo;  
35 -import android.print.PrintManager;  
36 -import android.print.pdf.PrintedPdfDocument;  
37 -import android.webkit.WebView;  
38 -import android.webkit.WebViewClient;  
39 21
40 -import androidx.core.content.FileProvider; 22 +import androidx.annotation.NonNull;
41 23
42 -import java.io.File;  
43 -import java.io.FileOutputStream;  
44 -import java.io.IOException;  
45 -import java.io.OutputStream;  
46 import java.util.HashMap; 24 import java.util.HashMap;
47 25
48 import io.flutter.plugin.common.MethodCall; 26 import io.flutter.plugin.common.MethodCall;
@@ -54,17 +32,11 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; @@ -54,17 +32,11 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
54 /** 32 /**
55 * PrintingPlugin 33 * PrintingPlugin
56 */ 34 */
57 -public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHandler {  
58 - private static PrintManager printManager; 35 +public class PrintingPlugin implements MethodCallHandler {
59 private final Activity activity; 36 private final Activity activity;
60 private final MethodChannel channel; 37 private final MethodChannel channel;
61 - private PrintedPdfDocument mPdfDocument;  
62 - private PrintJob printJob;  
63 - private byte[] documentData;  
64 - private String jobName;  
65 - private LayoutResultCallback callback;  
66 38
67 - private PrintingPlugin(Activity activity, MethodChannel channel) { 39 + private PrintingPlugin(@NonNull Activity activity, @NonNull MethodChannel channel) {
68 this.activity = activity; 40 this.activity = activity;
69 this.channel = channel; 41 this.channel = channel;
70 } 42 }
@@ -73,147 +45,66 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa @@ -73,147 +45,66 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
73 * Plugin registration. 45 * Plugin registration.
74 */ 46 */
75 public static void registerWith(Registrar registrar) { 47 public static void registerWith(Registrar registrar) {
76 - try {  
77 - final Activity activity = registrar.activity();  
78 -  
79 - if (activity == null) {  
80 - return;  
81 - }  
82 -  
83 - final MethodChannel channel =  
84 - new MethodChannel(registrar.messenger(), "net.nfet.printing");  
85 - final PrintingPlugin plugin = new PrintingPlugin(activity, channel);  
86 - channel.setMethodCallHandler(plugin);  
87 -  
88 - if (printManager == null) {  
89 - printManager = (PrintManager) activity.getSystemService(Context.PRINT_SERVICE);  
90 - }  
91 - } catch (Exception e) {  
92 - Log.e("PrintingPlugin", "Registration failed", e);  
93 - }  
94 - }  
95 -  
96 - @Override  
97 - public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor parcelFileDescriptor,  
98 - CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback) {  
99 - OutputStream output = null;  
100 - try {  
101 - output = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());  
102 - output.write(documentData, 0, documentData.length);  
103 - writeResultCallback.onWriteFinished(new PageRange[] {PageRange.ALL_PAGES});  
104 - } catch (IOException e) {  
105 - e.printStackTrace();  
106 - } finally {  
107 - try {  
108 - if (output != null) {  
109 - output.close();  
110 - }  
111 - } catch (IOException e) {  
112 - e.printStackTrace();  
113 - }  
114 - }  
115 - }  
116 -  
117 - @Override  
118 - public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,  
119 - CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {  
120 - // Create a new PdfDocument with the requested page attributes  
121 - mPdfDocument = new PrintedPdfDocument(activity, newAttributes);  
122 -  
123 - // Respond to cancellation request  
124 - if (cancellationSignal.isCanceled()) {  
125 - callback.onLayoutCancelled();  
126 - return;  
127 - }  
128 -  
129 - this.callback = callback;  
130 -  
131 - HashMap<String, Double> args = new HashMap<>();  
132 -  
133 - PrintAttributes.MediaSize size = newAttributes.getMediaSize();  
134 - args.put("width", size.getWidthMils() * 72.0 / 1000.0);  
135 - args.put("height", size.getHeightMils() * 72.0 / 1000.0);  
136 -  
137 - PrintAttributes.Margins margins = newAttributes.getMinMargins();  
138 - args.put("marginLeft", margins.getLeftMils() * 72.0 / 1000.0);  
139 - args.put("marginTop", margins.getTopMils() * 72.0 / 1000.0);  
140 - args.put("marginRight", margins.getRightMils() * 72.0 / 1000.0);  
141 - args.put("marginBottom", margins.getBottomMils() * 72.0 / 1000.0);  
142 -  
143 - channel.invokeMethod("onLayout", args);  
144 - }  
145 -  
146 - @Override  
147 - public void onFinish() {  
148 - try {  
149 - while (true) {  
150 - int state = printJob.getInfo().getState();  
151 -  
152 - if (state == PrintJobInfo.STATE_COMPLETED) {  
153 - HashMap<String, Object> args = new HashMap<>();  
154 - args.put("completed", true);  
155 - channel.invokeMethod("onCompleted", args);  
156 - break;  
157 - } else if (state == PrintJobInfo.STATE_CANCELED) {  
158 - HashMap<String, Object> args = new HashMap<>();  
159 - args.put("completed", false);  
160 - channel.invokeMethod("onCompleted", args);  
161 - break;  
162 - }  
163 -  
164 - Thread.sleep(200);  
165 - }  
166 - } catch (Exception e) {  
167 - HashMap<String, Object> args = new HashMap<>();  
168 - args.put("completed", printJob != null && printJob.isCompleted());  
169 - args.put("error", e.getMessage());  
170 - channel.invokeMethod("onCompleted", args); 48 + Activity activity = registrar.activity();
  49 + if (activity == null) {
  50 + return; // We can't print without an activity
171 } 51 }
172 52
173 - printJob = null;  
174 - mPdfDocument = null; 53 + final MethodChannel channel = new MethodChannel(registrar.messenger(), "net.nfet.printing");
  54 + channel.setMethodCallHandler(new PrintingPlugin(activity, channel));
175 } 55 }
176 56
177 @Override 57 @Override
178 - public void onMethodCall(MethodCall call, Result result) { 58 + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
179 switch (call.method) { 59 switch (call.method) {
180 - case "printPdf":  
181 - jobName =  
182 - call.argument("name") == null ? "Document" : (String) call.argument("name");  
183 - assert jobName != null;  
184 - printJob = printManager.print(jobName, this, null);  
185 - result.success(0); 60 + case "printPdf": {
  61 + final String name = call.argument("name");
  62 + final Double width = call.argument("width");
  63 + final Double height = call.argument("height");
  64 + final Double marginLeft = call.argument("marginLeft");
  65 + final Double marginTop = call.argument("marginTop");
  66 + final Double marginRight = call.argument("marginRight");
  67 + final Double marginBottom = call.argument("marginBottom");
  68 +
  69 + final PrintingJob printJob =
  70 + new PrintingJob(activity, this, (int) call.argument("job"));
  71 + assert name != null;
  72 + printJob.printPdf(
  73 + name, width, height, marginLeft, marginTop, marginRight, marginBottom);
  74 +
  75 + result.success(1);
186 break; 76 break;
187 - case "writePdf":  
188 - documentData = (byte[]) call.argument("doc");  
189 -  
190 - // Return print information to print framework  
191 - PrintDocumentInfo info =  
192 - new PrintDocumentInfo.Builder(jobName + ".pdf")  
193 - .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)  
194 - .build();  
195 -  
196 - // Content layout reflow is complete  
197 - callback.onLayoutFinished(info, true);  
198 -  
199 - result.success(0);  
200 - break;  
201 - case "cancelJob":  
202 - if (callback != null) callback.onLayoutCancelled();  
203 - if (printJob != null) printJob.cancel();  
204 - result.success(0); 77 + }
  78 + case "cancelJob": {
  79 + final PrintingJob printJob =
  80 + new PrintingJob(activity, this, (int) call.argument("job"));
  81 + printJob.cancelJob();
  82 + result.success(1);
205 break; 83 break;
206 - case "sharePdf":  
207 - sharePdf((byte[]) call.argument("doc"), (String) call.argument("name"));  
208 - result.success(0); 84 + }
  85 + case "sharePdf": {
  86 + final byte[] document = call.argument("doc");
  87 + final String name = call.argument("name");
  88 + PrintingJob.sharePdf(activity, document, name);
  89 + result.success(1);
209 break; 90 break;
210 - case "convertHtml":  
211 - double width = (double) call.argument("width");  
212 - double height = (double) call.argument("height");  
213 - double marginLeft = (double) call.argument("marginLeft");  
214 - double marginTop = (double) call.argument("marginTop");  
215 - double marginRight = (double) call.argument("marginRight");  
216 - double marginBottom = (double) call.argument("marginBottom"); 91 + }
  92 + case "convertHtml": {
  93 + Double width = call.argument("width");
  94 + Double height = call.argument("height");
  95 + Double marginLeft = call.argument("marginLeft");
  96 + Double marginTop = call.argument("marginTop");
  97 + Double marginRight = call.argument("marginRight");
  98 + Double marginBottom = call.argument("marginBottom");
  99 + final PrintingJob printJob =
  100 + new PrintingJob(activity, this, (int) call.argument("job"));
  101 +
  102 + assert width != null;
  103 + assert height != null;
  104 + assert marginLeft != null;
  105 + assert marginTop != null;
  106 + assert marginRight != null;
  107 + assert marginBottom != null;
217 108
218 PrintAttributes.Margins margins = 109 PrintAttributes.Margins margins =
219 new PrintAttributes.Margins(Double.valueOf(marginLeft * 1000.0).intValue(), 110 new PrintAttributes.Margins(Double.valueOf(marginLeft * 1000.0).intValue(),
@@ -225,90 +116,82 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa @@ -225,90 +116,82 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
225 "Provided size", Double.valueOf(width * 1000.0 / 72.0).intValue(), 116 "Provided size", Double.valueOf(width * 1000.0 / 72.0).intValue(),
226 Double.valueOf(height * 1000.0 / 72.0).intValue()); 117 Double.valueOf(height * 1000.0 / 72.0).intValue());
227 118
228 - convertHtml((String) call.argument("html"), size, margins, 119 + printJob.convertHtml((String) call.argument("html"), size, margins,
229 (String) call.argument("baseUrl")); 120 (String) call.argument("baseUrl"));
230 - result.success(0); 121 + result.success(1);
231 break; 122 break;
  123 + }
  124 + case "printingInfo": {
  125 + result.success(PrintingJob.printingInfo());
  126 + break;
  127 + }
232 default: 128 default:
233 result.notImplemented(); 129 result.notImplemented();
234 break; 130 break;
235 } 131 }
236 } 132 }
237 133
238 - private void sharePdf(byte[] data, String name) {  
239 - try {  
240 - final File externalFilesDirectory =  
241 - activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);  
242 - File shareFile;  
243 - if (name == null) {  
244 - shareFile = File.createTempFile("document-", ".pdf", externalFilesDirectory);  
245 - } else {  
246 - shareFile = new File(externalFilesDirectory, name);  
247 - } 134 + /// Request the Pdf document from flutter
  135 + void onLayout(final PrintingJob printJob, Double width, double height, double marginLeft,
  136 + double marginTop, double marginRight, double marginBottom) {
  137 + HashMap<String, Object> args = new HashMap<>();
  138 + args.put("width", width);
  139 + args.put("height", height);
248 140
249 - FileOutputStream stream = new FileOutputStream(shareFile);  
250 - stream.write(data);  
251 - stream.close(); 141 + args.put("marginLeft", marginLeft);
  142 + args.put("marginTop", marginTop);
  143 + args.put("marginRight", marginRight);
  144 + args.put("marginBottom", marginBottom);
  145 + args.put("job", printJob.index);
  146 +
  147 + channel.invokeMethod("onLayout", args, new Result() {
  148 + @Override
  149 + public void success(Object result) {
  150 + if (result instanceof byte[]) {
  151 + printJob.setDocument((byte[]) result);
  152 + } else {
  153 + printJob.cancelJob();
  154 + }
  155 + }
252 156
253 - Uri apkURI = FileProvider.getUriForFile(activity,  
254 - activity.getApplicationContext().getPackageName() + ".flutter.printing",  
255 - shareFile); 157 + @Override
  158 + public void error(String errorCode, String errorMessage, Object errorDetails) {
  159 + printJob.cancelJob();
  160 + }
256 161
257 - Intent shareIntent = new Intent();  
258 - shareIntent.setAction(Intent.ACTION_SEND);  
259 - shareIntent.setType("application/pdf");  
260 - shareIntent.putExtra(Intent.EXTRA_STREAM, apkURI);  
261 - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);  
262 - Intent chooserIntent = Intent.createChooser(shareIntent, null);  
263 - activity.startActivity(chooserIntent);  
264 - shareFile.deleteOnExit();  
265 - } catch (IOException e) {  
266 - e.printStackTrace();  
267 - } 162 + @Override
  163 + public void notImplemented() {
  164 + printJob.cancelJob();
  165 + }
  166 + });
268 } 167 }
269 168
270 - private void convertHtml(String data, final PrintAttributes.MediaSize size,  
271 - final PrintAttributes.Margins margins, String baseUrl) {  
272 - final WebView webView = new WebView(activity.getApplicationContext()); 169 + /// send completion status to flutter
  170 + void onCompleted(PrintingJob printJob, boolean completed, String error) {
  171 + HashMap<String, Object> args = new HashMap<>();
  172 + args.put("completed", completed);
273 173
274 - webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null); 174 + args.put("error", error);
  175 + args.put("job", printJob.index);
275 176
276 - webView.setWebViewClient(new WebViewClient() {  
277 - @Override  
278 - public void onPageFinished(WebView view, String url) {  
279 - super.onPageFinished(view, url);  
280 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  
281 - PrintAttributes attributes =  
282 - new PrintAttributes.Builder()  
283 - .setMediaSize(size)  
284 - .setResolution(  
285 - new PrintAttributes.Resolution("pdf", "pdf", 600, 600))  
286 - .setMinMargins(margins)  
287 - .build(); 177 + channel.invokeMethod("onCompleted", args);
  178 + }
288 179
289 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {  
290 - final PrintDocumentAdapter adapter =  
291 - webView.createPrintDocumentAdapter("printing"); 180 + /// send html to pdf data result to flutter
  181 + void onHtmlRendered(PrintingJob printJob, byte[] pdfData) {
  182 + HashMap<String, Object> args = new HashMap<>();
  183 + args.put("doc", pdfData);
  184 + args.put("job", printJob.index);
292 185
293 - PdfConvert.print(activity, adapter, attributes, new PdfConvert.Result() {  
294 - @Override  
295 - public void onSuccess(File file) {  
296 - try {  
297 - byte[] fileContent = PdfConvert.readFile(file);  
298 - channel.invokeMethod("onHtmlRendered", fileContent);  
299 - } catch (IOException e) {  
300 - onError(e.getMessage());  
301 - }  
302 - } 186 + channel.invokeMethod("onHtmlRendered", args);
  187 + }
303 188
304 - @Override  
305 - public void onError(String message) {  
306 - channel.invokeMethod("onHtmlError", message);  
307 - }  
308 - });  
309 - }  
310 - }  
311 - }  
312 - }); 189 + /// send html to pdf conversion error to flutter
  190 + void onHtmlError(PrintingJob printJob, String error) {
  191 + HashMap<String, Object> args = new HashMap<>();
  192 + args.put("error", error);
  193 + args.put("job", printJob.index);
  194 +
  195 + channel.invokeMethod("onHtmlError", args);
313 } 196 }
314 } 197 }
@@ -68,6 +68,9 @@ @@ -68,6 +68,9 @@
68 **/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 68 **/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
69 **/ios/Podfile.lock 69 **/ios/Podfile.lock
70 **/ios/Podfile 70 **/ios/Podfile
  71 +**/ios/Flutter/Flutter.podspec
  72 +lib/generated_plugin_registrant.dart
  73 +**/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
71 74
72 # Exceptions to above rules. 75 # Exceptions to above rules.
73 !**/ios/**/default.mode1v3 76 !**/ios/**/default.mode1v3
@@ -180,13 +180,13 @@ @@ -180,13 +180,13 @@
180 TargetAttributes = { 180 TargetAttributes = {
181 97C146ED1CF9000F007C117D = { 181 97C146ED1CF9000F007C117D = {
182 CreatedOnToolsVersion = 7.3.1; 182 CreatedOnToolsVersion = 7.3.1;
183 - LastSwiftMigration = 1010; 183 + LastSwiftMigration = 1120;
184 }; 184 };
185 }; 185 };
186 }; 186 };
187 buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 187 buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
188 compatibilityVersion = "Xcode 3.2"; 188 compatibilityVersion = "Xcode 3.2";
189 - developmentRegion = English; 189 + developmentRegion = en;
190 hasScannedForEncodings = 0; 190 hasScannedForEncodings = 0;
191 knownRegions = ( 191 knownRegions = (
192 en, 192 en,
@@ -386,7 +386,7 @@ @@ -386,7 +386,7 @@
386 ); 386 );
387 PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample; 387 PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample;
388 PRODUCT_NAME = "$(TARGET_NAME)"; 388 PRODUCT_NAME = "$(TARGET_NAME)";
389 - SWIFT_VERSION = 4.2; 389 + SWIFT_VERSION = 5.0;
390 VERSIONING_SYSTEM = "apple-generic"; 390 VERSIONING_SYSTEM = "apple-generic";
391 }; 391 };
392 name = Profile; 392 name = Profile;
@@ -520,7 +520,7 @@ @@ -520,7 +520,7 @@
520 PRODUCT_NAME = "$(TARGET_NAME)"; 520 PRODUCT_NAME = "$(TARGET_NAME)";
521 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 521 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
522 SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 522 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
523 - SWIFT_VERSION = 4.2; 523 + SWIFT_VERSION = 5.0;
524 VERSIONING_SYSTEM = "apple-generic"; 524 VERSIONING_SYSTEM = "apple-generic";
525 }; 525 };
526 name = Debug; 526 name = Debug;
@@ -546,7 +546,7 @@ @@ -546,7 +546,7 @@
546 PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample; 546 PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample;
547 PRODUCT_NAME = "$(TARGET_NAME)"; 547 PRODUCT_NAME = "$(TARGET_NAME)";
548 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 548 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
549 - SWIFT_VERSION = 4.2; 549 + SWIFT_VERSION = 5.0;
550 VERSIONING_SYSTEM = "apple-generic"; 550 VERSIONING_SYSTEM = "apple-generic";
551 }; 551 };
552 name = Release; 552 name = Release;
@@ -31,6 +31,30 @@ class MyAppState extends State<MyApp> { @@ -31,6 +31,30 @@ class MyAppState extends State<MyApp> {
31 final GlobalKey<State<StatefulWidget>> previewContainer = GlobalKey(); 31 final GlobalKey<State<StatefulWidget>> previewContainer = GlobalKey();
32 32
33 Printer selectedPrinter; 33 Printer selectedPrinter;
  34 + PrintingInfo printingInfo;
  35 +
  36 + @override
  37 + void initState() {
  38 + Printing.info().then((PrintingInfo info) {
  39 + setState(() {
  40 + printingInfo = info;
  41 + });
  42 + });
  43 + super.initState();
  44 + }
  45 +
  46 + void _showPrintedToast(bool printed) {
  47 + final ScaffoldState scaffold = Scaffold.of(shareWidget.currentContext);
  48 + if (printed) {
  49 + scaffold.showSnackBar(const SnackBar(
  50 + content: Text('Document printed successfully'),
  51 + ));
  52 + } else {
  53 + scaffold.showSnackBar(const SnackBar(
  54 + content: Text('Document not printed'),
  55 + ));
  56 + }
  57 + }
34 58
35 Future<void> _printPdf() async { 59 Future<void> _printPdf() async {
36 print('Print ...'); 60 print('Print ...');
@@ -38,7 +62,7 @@ class MyAppState extends State<MyApp> { @@ -38,7 +62,7 @@ class MyAppState extends State<MyApp> {
38 onLayout: (PdfPageFormat format) async => 62 onLayout: (PdfPageFormat format) async =>
39 (await generateDocument(format)).save()); 63 (await generateDocument(format)).save());
40 64
41 - print('Document printed: $result'); 65 + _showPrintedToast(result);
42 } 66 }
43 67
44 Future<void> _saveAsFile() async { 68 Future<void> _saveAsFile() async {
@@ -84,7 +108,7 @@ class MyAppState extends State<MyApp> { @@ -84,7 +108,7 @@ class MyAppState extends State<MyApp> {
84 onLayout: (PdfPageFormat format) async => 108 onLayout: (PdfPageFormat format) async =>
85 (await generateDocument(PdfPageFormat.letter)).save()); 109 (await generateDocument(PdfPageFormat.letter)).save());
86 110
87 - print('Document printed: $result'); 111 + _showPrintedToast(result);
88 } 112 }
89 113
90 Future<void> _sharePdf() async { 114 Future<void> _sharePdf() async {
@@ -112,7 +136,8 @@ class MyAppState extends State<MyApp> { @@ -112,7 +136,8 @@ class MyAppState extends State<MyApp> {
112 await im.toByteData(format: ui.ImageByteFormat.rawRgba); 136 await im.toByteData(format: ui.ImageByteFormat.rawRgba);
113 print('Print Screen ${im.width}x${im.height} ...'); 137 print('Print Screen ${im.width}x${im.height} ...');
114 138
115 - Printing.layoutPdf(onLayout: (PdfPageFormat format) { 139 + final bool result =
  140 + await Printing.layoutPdf(onLayout: (PdfPageFormat format) {
116 final pdf.Document document = pdf.Document(); 141 final pdf.Document document = pdf.Document();
117 142
118 final PdfImage image = PdfImage(document.document, 143 final PdfImage image = PdfImage(document.document,
@@ -132,24 +157,32 @@ class MyAppState extends State<MyApp> { @@ -132,24 +157,32 @@ class MyAppState extends State<MyApp> {
132 157
133 return document.save(); 158 return document.save();
134 }); 159 });
  160 +
  161 + _showPrintedToast(result);
135 } 162 }
136 163
137 Future<void> _printHtml() async { 164 Future<void> _printHtml() async {
138 print('Print html ...'); 165 print('Print html ...');
139 - await Printing.layoutPdf(onLayout: (PdfPageFormat format) async { 166 + final bool result =
  167 + await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
140 final String html = await rootBundle.loadString('assets/example.html'); 168 final String html = await rootBundle.loadString('assets/example.html');
141 return await Printing.convertHtml(format: format, html: html); 169 return await Printing.convertHtml(format: format, html: html);
142 }); 170 });
  171 +
  172 + _showPrintedToast(result);
143 } 173 }
144 174
145 Future<void> _printMarkdown() async { 175 Future<void> _printMarkdown() async {
146 print('Print Markdown ...'); 176 print('Print Markdown ...');
147 - await Printing.layoutPdf(onLayout: (PdfPageFormat format) async { 177 + final bool result =
  178 + await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
148 final String md = await rootBundle.loadString('assets/example.md'); 179 final String md = await rootBundle.loadString('assets/example.md');
149 final String html = markdown.markdownToHtml(md, 180 final String html = markdown.markdownToHtml(md,
150 extensionSet: markdown.ExtensionSet.gitHubWeb); 181 extensionSet: markdown.ExtensionSet.gitHubWeb);
151 return await Printing.convertHtml(format: format, html: html); 182 return await Printing.convertHtml(format: format, html: html);
152 }); 183 });
  184 +
  185 + _showPrintedToast(result);
153 } 186 }
154 187
155 @override 188 @override
@@ -170,37 +203,52 @@ class MyAppState extends State<MyApp> { @@ -170,37 +203,52 @@ class MyAppState extends State<MyApp> {
170 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 203 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
171 children: <Widget>[ 204 children: <Widget>[
172 RaisedButton( 205 RaisedButton(
173 - child: const Text('Print Document'), onPressed: _printPdf), 206 + child: const Text('Print Document'),
  207 + onPressed: printingInfo?.canPrint ?? false ? _printPdf : null,
  208 + ),
174 Row( 209 Row(
175 mainAxisSize: MainAxisSize.min, 210 mainAxisSize: MainAxisSize.min,
176 children: <Widget>[ 211 children: <Widget>[
177 RaisedButton( 212 RaisedButton(
178 - key: pickWidget,  
179 - child: const Text('Pick Printer'),  
180 - onPressed: _pickPrinter), 213 + key: pickWidget,
  214 + child: const Text('Pick Printer'),
  215 + onPressed: printingInfo?.directPrint ?? false
  216 + ? _pickPrinter
  217 + : null,
  218 + ),
181 const SizedBox(width: 10), 219 const SizedBox(width: 10),
182 RaisedButton( 220 RaisedButton(
183 - child: Text(selectedPrinter == null  
184 - ? 'Direct Print'  
185 - : 'Print to $selectedPrinter'),  
186 - onPressed:  
187 - selectedPrinter != null ? _directPrintPdf : null), 221 + child: Text(selectedPrinter == null
  222 + ? 'Direct Print'
  223 + : 'Print to $selectedPrinter'),
  224 + onPressed:
  225 + selectedPrinter != null ? _directPrintPdf : null,
  226 + ),
188 ], 227 ],
189 ), 228 ),
190 RaisedButton( 229 RaisedButton(
191 - key: shareWidget,  
192 - child: const Text('Share Document'),  
193 - onPressed: _sharePdf), 230 + key: shareWidget,
  231 + child: const Text('Share Document'),
  232 + onPressed: printingInfo?.canShare ?? false ? _sharePdf : null,
  233 + ),
194 RaisedButton( 234 RaisedButton(
195 - child: const Text('Print Screenshot'),  
196 - onPressed: _printScreen), 235 + child: const Text('Print Screenshot'),
  236 + onPressed:
  237 + printingInfo?.canPrint ?? false ? _printScreen : null,
  238 + ),
197 RaisedButton( 239 RaisedButton(
198 child: const Text('Save to file'), onPressed: _saveAsFile), 240 child: const Text('Save to file'), onPressed: _saveAsFile),
199 RaisedButton( 241 RaisedButton(
200 - child: const Text('Print Html'), onPressed: _printHtml), 242 + child: const Text('Print Html'),
  243 + onPressed:
  244 + printingInfo?.canConvertHtml ?? false ? _printHtml : null,
  245 + ),
201 RaisedButton( 246 RaisedButton(
202 - child: const Text('Print Markdown'),  
203 - onPressed: _printMarkdown), 247 + child: const Text('Print Markdown'),
  248 + onPressed: printingInfo?.canConvertHtml ?? false
  249 + ? _printMarkdown
  250 + : null,
  251 + ),
204 if (canDebug) 252 if (canDebug)
205 Row( 253 Row(
206 mainAxisSize: MainAxisSize.min, 254 mainAxisSize: MainAxisSize.min,
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 -import Flutter  
18 -  
19 -func dataProviderReleaseDataCallback(info _: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size _: Int) {  
20 - data.deallocate()  
21 -}  
22 -  
23 -class PdfPrintPageRenderer: UIPrintPageRenderer {  
24 - private var channel: FlutterMethodChannel?  
25 - private var pdfDocument: CGPDFDocument?  
26 - private var lock: NSLock?  
27 - private var mustLayout: Bool = true  
28 -  
29 - init(_ channel: FlutterMethodChannel?, data: Data?) {  
30 - super.init()  
31 - self.channel = channel  
32 - pdfDocument = nil  
33 - if data != nil {  
34 - setDocument(data)  
35 - mustLayout = false  
36 - }  
37 - lock = NSLock()  
38 - }  
39 -  
40 - override func drawPage(at pageIndex: Int, in _: CGRect) {  
41 - let ctx = UIGraphicsGetCurrentContext()  
42 - let page = pdfDocument?.page(at: pageIndex + 1)  
43 - ctx?.scaleBy(x: 1.0, y: -1.0)  
44 - ctx?.translateBy(x: 0.0, y: -paperRect.size.height)  
45 - if page != nil {  
46 - ctx?.drawPDFPage(page!)  
47 - }  
48 - }  
49 -  
50 - func cancelJob() {  
51 - pdfDocument = nil  
52 - lock?.unlock()  
53 - }  
54 -  
55 - func setDocument(_ data: Data?) {  
56 - let bytesPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data?.count ?? 0)  
57 - data?.copyBytes(to: bytesPointer, count: data?.count ?? 0)  
58 - let dataProvider = CGDataProvider(dataInfo: nil, data: bytesPointer, size: data?.count ?? 0, releaseData: dataProviderReleaseDataCallback)  
59 - pdfDocument = CGPDFDocument(dataProvider!)  
60 - lock?.unlock()  
61 - }  
62 -  
63 - override var numberOfPages: Int {  
64 - let width = NSNumber(value: Double(paperRect.size.width))  
65 - let height = NSNumber(value: Double(paperRect.size.height))  
66 - let marginLeft = NSNumber(value: Double(printableRect.origin.x))  
67 - let marginTop = NSNumber(value: Double(printableRect.origin.y))  
68 - let marginRight = NSNumber(value: Double(paperRect.size.width - (printableRect.origin.x + printableRect.size.width)))  
69 - let marginBottom = NSNumber(value: Double(paperRect.size.height - (printableRect.origin.y + printableRect.size.height)))  
70 -  
71 - let arg = [  
72 - "width": width,  
73 - "height": height,  
74 - "marginLeft": marginLeft,  
75 - "marginTop": marginTop,  
76 - "marginRight": marginRight,  
77 - "marginBottom": marginBottom,  
78 - ]  
79 -  
80 - if mustLayout {  
81 - lock?.lock()  
82 - channel?.invokeMethod("onLayout", arguments: arg)  
83 - lock?.lock()  
84 - lock?.unlock()  
85 - }  
86 -  
87 - let pages = pdfDocument?.numberOfPages ?? 0  
88 -  
89 - return pages  
90 - }  
91 -  
92 - var pageArgs: [String: NSNumber] {  
93 - let width = NSNumber(value: Double(paperRect.size.width))  
94 - let height = NSNumber(value: Double(paperRect.size.height))  
95 - let marginLeft = NSNumber(value: Double(printableRect.origin.x))  
96 - let marginTop = NSNumber(value: Double(printableRect.origin.y))  
97 - let marginRight = NSNumber(value: Double(paperRect.size.width - (printableRect.origin.x + printableRect.size.width)))  
98 - let marginBottom = NSNumber(value: Double(paperRect.size.height - (printableRect.origin.y + printableRect.size.height)))  
99 -  
100 - return [  
101 - "width": width,  
102 - "height": height,  
103 - "marginLeft": marginLeft,  
104 - "marginTop": marginTop,  
105 - "marginRight": marginRight,  
106 - "marginBottom": marginBottom,  
107 - ]  
108 - }  
109 -}  
  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 +import Flutter
  18 +import WebKit
  19 +
  20 +func dataProviderReleaseDataCallback(info _: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size _: Int) {
  21 + data.deallocate()
  22 +}
  23 +
  24 +public class PrintJob: UIPrintPageRenderer, UIPrintInteractionControllerDelegate {
  25 + private var printing: PrintingPlugin
  26 + public var index: Int
  27 + private var pdfDocument: CGPDFDocument?
  28 + private var urlObservation: NSKeyValueObservation?
  29 + private var jobName: String?
  30 +
  31 + public init(printing: PrintingPlugin, index: Int) {
  32 + self.printing = printing
  33 + self.index = index
  34 + pdfDocument = nil
  35 + super.init()
  36 + }
  37 +
  38 + public override func drawPage(at pageIndex: Int, in _: CGRect) {
  39 + let ctx = UIGraphicsGetCurrentContext()
  40 + let page = pdfDocument?.page(at: pageIndex + 1)
  41 + ctx?.scaleBy(x: 1.0, y: -1.0)
  42 + ctx?.translateBy(x: 0.0, y: -paperRect.size.height)
  43 + if page != nil {
  44 + ctx?.drawPDFPage(page!)
  45 + }
  46 + }
  47 +
  48 + func cancelJob() {
  49 + pdfDocument = nil
  50 + }
  51 +
  52 + func setDocument(_ data: Data?) {
  53 + let bytesPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data?.count ?? 0)
  54 + data?.copyBytes(to: bytesPointer, count: data?.count ?? 0)
  55 + let dataProvider = CGDataProvider(dataInfo: nil, data: bytesPointer, size: data?.count ?? 0, releaseData: dataProviderReleaseDataCallback)
  56 + pdfDocument = CGPDFDocument(dataProvider!)
  57 +
  58 + let controller = UIPrintInteractionController.shared
  59 + controller.delegate = self
  60 +
  61 + let printInfo = UIPrintInfo.printInfo()
  62 + printInfo.jobName = jobName!
  63 + printInfo.outputType = .general
  64 + controller.printInfo = printInfo
  65 + controller.printPageRenderer = self
  66 + controller.present(animated: true, completionHandler: completionHandler)
  67 + }
  68 +
  69 + public override var numberOfPages: Int {
  70 + let pages = pdfDocument?.numberOfPages ?? 0
  71 + return pages
  72 + }
  73 +
  74 + func completionHandler(printController _: UIPrintInteractionController, completed: Bool, error: Error?) {
  75 + if !completed, error != nil {
  76 + print("Unable to print: \(error?.localizedDescription ?? "unknown error")")
  77 + }
  78 +
  79 + printing.onCompleted(printJob: self, completed: completed, error: error?.localizedDescription as NSString?)
  80 + }
  81 +
  82 + func directPrintPdf(name: String, data: Data, withPrinter printerID: String) {
  83 + let printing = UIPrintInteractionController.isPrintingAvailable
  84 + if !printing {
  85 + self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
  86 + return
  87 + }
  88 +
  89 + let controller = UIPrintInteractionController.shared
  90 +
  91 + let printInfo = UIPrintInfo.printInfo()
  92 + printInfo.jobName = name
  93 + printInfo.outputType = .general
  94 + controller.printInfo = printInfo
  95 + controller.printingItem = data
  96 + let printerURL = URL(string: printerID)
  97 +
  98 + if printerURL == nil {
  99 + self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
  100 + return
  101 + }
  102 +
  103 + let printer = UIPrinter(url: printerURL!)
  104 + controller.print(to: printer, completionHandler: completionHandler)
  105 + }
  106 +
  107 + func printPdf(name: String, withPageSize size: CGSize, andMargin margin: CGRect) {
  108 + let printing = UIPrintInteractionController.isPrintingAvailable
  109 + if !printing {
  110 + self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
  111 + return
  112 + }
  113 +
  114 + jobName = name
  115 +
  116 + self.printing.onLayout(
  117 + printJob: self,
  118 + width: size.width,
  119 + height: size.height,
  120 + marginLeft: margin.minX,
  121 + marginTop: margin.minY,
  122 + marginRight: size.width - margin.maxX,
  123 + marginBottom: size.height - margin.maxY
  124 + )
  125 + }
  126 +
  127 + static func sharePdf(data: Data, withSourceRect rect: CGRect, andName name: String) {
  128 + let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
  129 + let fileURL = tmpDirURL.appendingPathComponent(name)
  130 +
  131 + do {
  132 + try data.write(to: fileURL, options: .atomic)
  133 + } catch {
  134 + print("sharePdf error: \(error.localizedDescription)")
  135 + return
  136 + }
  137 +
  138 + let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
  139 + if UI_USER_INTERFACE_IDIOM() == .pad {
  140 + let controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
  141 + activityViewController.popoverPresentationController?.sourceView = controller?.view
  142 + activityViewController.popoverPresentationController?.sourceRect = rect
  143 + }
  144 + UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)
  145 + }
  146 +
  147 + func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {
  148 + let viewController = UIApplication.shared.delegate?.window?!.rootViewController
  149 + let wkWebView = WKWebView(frame: viewController!.view.bounds)
  150 + wkWebView.isHidden = true
  151 + wkWebView.tag = 100
  152 + viewController?.view.addSubview(wkWebView)
  153 + wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)
  154 +
  155 + urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in
  156 + // this is workaround for issue with loading local images
  157 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  158 + // assign the print formatter to the print page renderer
  159 + let renderer = UIPrintPageRenderer()
  160 + renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)
  161 +
  162 + // assign paperRect and printableRect values
  163 + renderer.setValue(rect, forKey: "paperRect")
  164 + renderer.setValue(margin, forKey: "printableRect")
  165 +
  166 + // create pdf context and draw each page
  167 + let pdfData = NSMutableData()
  168 + UIGraphicsBeginPDFContextToData(pdfData, rect, nil)
  169 +
  170 + for i in 0 ..< renderer.numberOfPages {
  171 + UIGraphicsBeginPDFPage()
  172 + renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
  173 + }
  174 +
  175 + UIGraphicsEndPDFContext()
  176 +
  177 + if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {
  178 + viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated
  179 +
  180 + // clear WKWebView cache
  181 + if #available(iOS 9.0, *) {
  182 + WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
  183 + records.forEach { record in
  184 + WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
  185 + }
  186 + }
  187 + }
  188 + }
  189 +
  190 + // dispose urlObservation
  191 + self.urlObservation = nil
  192 + self.printing.onHtmlRendered(printJob: self, pdfData: pdfData as Data)
  193 + }
  194 + })
  195 + }
  196 +
  197 + static func pickPrinter(result: @escaping FlutterResult, withSourceRect rect: CGRect) {
  198 + let controller = UIPrinterPickerController(initiallySelectedPrinter: nil)
  199 +
  200 + let pickPrinterCompletionHandler: UIPrinterPickerController.CompletionHandler = {
  201 + (printerPickerController: UIPrinterPickerController, completed: Bool, error: Error?) in
  202 + if !completed, error != nil {
  203 + print("Unable to pick printer: \(error?.localizedDescription ?? "unknown error")")
  204 + result(nil)
  205 + return
  206 + }
  207 +
  208 + if printerPickerController.selectedPrinter == nil {
  209 + result(nil)
  210 + return
  211 + }
  212 +
  213 + let printer = printerPickerController.selectedPrinter!
  214 + let data: NSDictionary = [
  215 + "url": printer.url.absoluteString as Any,
  216 + "name": printer.displayName as Any,
  217 + "model": printer.makeAndModel as Any,
  218 + "location": printer.displayLocation as Any,
  219 + ]
  220 + result(data)
  221 + }
  222 +
  223 + if UI_USER_INTERFACE_IDIOM() == .pad {
  224 + let viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
  225 + if viewController != nil {
  226 + controller.present(from: rect, in: viewController!.view, animated: true, completionHandler: pickPrinterCompletionHandler)
  227 + return
  228 + }
  229 + }
  230 +
  231 + controller.present(animated: true, completionHandler: pickPrinterCompletionHandler)
  232 + }
  233 +
  234 + public static func printingInfo() -> NSDictionary {
  235 + let data: NSDictionary = [
  236 + "directPrint": true,
  237 + "dynamicLayout": false,
  238 + "canPrint": true,
  239 + "canConvertHtml": true,
  240 + "canShare": true,
  241 + ]
  242 + return data
  243 + }
  244 +}
@@ -15,57 +15,66 @@ @@ -15,57 +15,66 @@
15 */ 15 */
16 16
17 import Flutter 17 import Flutter
18 -import UIKit  
19 -import WebKit 18 +import Foundation
20 19
21 -public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControllerDelegate {  
22 - private var channel: FlutterMethodChannel?  
23 - private var renderer: PdfPrintPageRenderer?  
24 - private var urlObservation: NSKeyValueObservation? 20 +public class PrintingPlugin: NSObject, FlutterPlugin {
  21 + private var channel: FlutterMethodChannel
25 22
26 - init(_ channel: FlutterMethodChannel?) {  
27 - super.init() 23 + init(_ channel: FlutterMethodChannel) {
28 self.channel = channel 24 self.channel = channel
29 - renderer = nil 25 + super.init()
30 } 26 }
31 27
  28 + /// Entry point
32 public static func register(with registrar: FlutterPluginRegistrar) { 29 public static func register(with registrar: FlutterPluginRegistrar) {
33 let channel = FlutterMethodChannel(name: "net.nfet.printing", binaryMessenger: registrar.messenger()) 30 let channel = FlutterMethodChannel(name: "net.nfet.printing", binaryMessenger: registrar.messenger())
34 let instance = PrintingPlugin(channel) 31 let instance = PrintingPlugin(channel)
35 registrar.addMethodCallDelegate(instance, channel: channel) 32 registrar.addMethodCallDelegate(instance, channel: channel)
36 } 33 }
37 34
  35 + /// Flutter method handlers
38 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 36 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
39 let args = call.arguments! as! [String: Any] 37 let args = call.arguments! as! [String: Any]
40 if call.method == "printPdf" { 38 if call.method == "printPdf" {
41 - let name = args["name"] as? String ?? ""  
42 - let object = args["doc"] as? FlutterStandardTypedData  
43 - printPdf(name, data: object?.data) 39 + let name = args["name"] as! String
  40 + let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0)
  41 + let height = CGFloat((args["height"] as? NSNumber)?.floatValue ?? 0.0)
  42 + let marginLeft = CGFloat((args["marginLeft"] as? NSNumber)?.floatValue ?? 0.0)
  43 + let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0)
  44 + let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0)
  45 + let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0)
  46 + let printJob = PrintJob(printing: self, index: args["job"] as! Int)
  47 + printJob.printPdf(name: name,
  48 + withPageSize: CGSize(
  49 + width: width,
  50 + height: height
  51 + ),
  52 + andMargin: CGRect(
  53 + x: marginLeft,
  54 + y: marginTop,
  55 + width: width - marginRight - marginLeft,
  56 + height: height - marginBottom - marginTop
  57 + ))
44 result(NSNumber(value: 1)) 58 result(NSNumber(value: 1))
45 } else if call.method == "directPrintPdf" { 59 } else if call.method == "directPrintPdf" {
46 - let name = args["name"] as? String ?? ""  
47 - let printer = args["printer"] as? String  
48 - let object = args["doc"] as? FlutterStandardTypedData  
49 - directPrintPdf(name: name, data: object!.data, withPrinter: printer!)  
50 - result(NSNumber(value: 1))  
51 - } else if call.method == "writePdf" {  
52 - if let object = args["doc"] as? FlutterStandardTypedData {  
53 - writePdf(object.data)  
54 - }  
55 - result(NSNumber(value: 1))  
56 - } else if call.method == "cancelJob" {  
57 - renderer?.cancelJob()  
58 - let controller = UIPrintInteractionController.shared  
59 - controller.dismiss(animated: true) 60 + let name = args["name"] as! String
  61 + let printer = args["printer"] as! String
  62 + let object = args["doc"] as! FlutterStandardTypedData
  63 + let printJob = PrintJob(printing: self, index: args["job"] as! Int)
  64 + printJob.directPrintPdf(name: name, data: object.data, withPrinter: printer)
60 result(NSNumber(value: 1)) 65 result(NSNumber(value: 1))
61 } else if call.method == "sharePdf" { 66 } else if call.method == "sharePdf" {
62 - if let object = args["doc"] as? FlutterStandardTypedData {  
63 - sharePdf(  
64 - object,  
65 - withSourceRect: CGRect(x: CGFloat((args["x"] as? NSNumber)?.floatValue ?? 0.0), y: CGFloat((args["y"] as? NSNumber)?.floatValue ?? 0.0), width: CGFloat((args["w"] as? NSNumber)?.floatValue ?? 0.0), height: CGFloat((args["h"] as? NSNumber)?.floatValue ?? 0.0)),  
66 - andName: args["name"] as? String  
67 - )  
68 - } 67 + let object = args["doc"] as! FlutterStandardTypedData
  68 + PrintJob.sharePdf(
  69 + data: object.data,
  70 + withSourceRect: CGRect(
  71 + x: CGFloat((args["x"] as? NSNumber)?.floatValue ?? 0.0),
  72 + y: CGFloat((args["y"] as? NSNumber)?.floatValue ?? 0.0),
  73 + width: CGFloat((args["w"] as? NSNumber)?.floatValue ?? 0.0),
  74 + height: CGFloat((args["h"] as? NSNumber)?.floatValue ?? 0.0)
  75 + ),
  76 + andName: args["name"] as! String
  77 + )
69 result(NSNumber(value: 1)) 78 result(NSNumber(value: 1))
70 } else if call.method == "convertHtml" { 79 } else if call.method == "convertHtml" {
71 let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0) 80 let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0)
@@ -74,8 +83,10 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll @@ -74,8 +83,10 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll
74 let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0) 83 let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0)
75 let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0) 84 let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0)
76 let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0) 85 let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0)
77 - convertHtml(  
78 - (args["html"] as? String)!, 86 + let printJob = PrintJob(printing: self, index: args["job"] as! Int)
  87 +
  88 + printJob.convertHtml(
  89 + args["html"] as! String,
79 withPageSize: CGRect( 90 withPageSize: CGRect(
80 x: 0.0, 91 x: 0.0,
81 y: 0.0, 92 y: 0.0,
@@ -88,219 +99,70 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll @@ -88,219 +99,70 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll
88 width: width - marginRight - marginLeft, 99 width: width - marginRight - marginLeft,
89 height: height - marginBottom - marginTop 100 height: height - marginBottom - marginTop
90 ), 101 ),
91 - andBaseUrl: args["baseUrl"] as? String == nil ? nil : URL(string: (args["baseUrl"] as? String)!) 102 + andBaseUrl: args["baseUrl"] as? String == nil ? nil : URL(string: args["baseUrl"] as! String)
92 ) 103 )
93 result(NSNumber(value: 1)) 104 result(NSNumber(value: 1))
94 } else if call.method == "pickPrinter" { 105 } else if call.method == "pickPrinter" {
95 - pickPrinter(result, withSourceRect: CGRect( 106 + PrintJob.pickPrinter(result: result, withSourceRect: CGRect(
96 x: CGFloat((args["x"] as? NSNumber)?.floatValue ?? 0.0), 107 x: CGFloat((args["x"] as? NSNumber)?.floatValue ?? 0.0),
97 y: CGFloat((args["y"] as? NSNumber)?.floatValue ?? 0.0), 108 y: CGFloat((args["y"] as? NSNumber)?.floatValue ?? 0.0),
98 width: CGFloat((args["w"] as? NSNumber)?.floatValue ?? 0.0), 109 width: CGFloat((args["w"] as? NSNumber)?.floatValue ?? 0.0),
99 height: CGFloat((args["h"] as? NSNumber)?.floatValue ?? 0.0) 110 height: CGFloat((args["h"] as? NSNumber)?.floatValue ?? 0.0)
100 )) 111 ))
101 } else if call.method == "printingInfo" { 112 } else if call.method == "printingInfo" {
102 - let data: NSDictionary = [  
103 - "iosVersion": UIDevice.current.systemVersion,  
104 - ]  
105 - result(data) 113 + result(PrintJob.printingInfo())
106 } else { 114 } else {
107 result(FlutterMethodNotImplemented) 115 result(FlutterMethodNotImplemented)
108 } 116 }
109 } 117 }
110 118
111 - func completionHandler(printController _: UIPrintInteractionController, completed: Bool, error: Error?) {  
112 - if !completed, error != nil {  
113 - print("Unable to print: \(error?.localizedDescription ?? "unknown error")")  
114 - } 119 + /// Request the Pdf document from flutter
  120 + public func onLayout(printJob: PrintJob, width: CGFloat, height: CGFloat, marginLeft: CGFloat, marginTop: CGFloat, marginRight: CGFloat, marginBottom: CGFloat) {
  121 + let arg = [
  122 + "width": width,
  123 + "height": height,
  124 + "marginLeft": marginLeft,
  125 + "marginTop": marginTop,
  126 + "marginRight": marginRight,
  127 + "marginBottom": marginBottom,
  128 + "job": printJob.index,
  129 + ] as [String: Any]
  130 +
  131 + channel.invokeMethod("onLayout", arguments: arg, result: { (result: Any?) -> Void in
  132 + if result as? Bool == false {
  133 + printJob.cancelJob()
  134 + } else {
  135 + let object = result as! FlutterStandardTypedData
  136 + printJob.setDocument(object.data)
  137 + }
  138 + })
  139 + }
115 140
  141 + /// send completion status to flutter
  142 + public func onCompleted(printJob: PrintJob, completed: Bool, error: NSString?) {
116 let data: NSDictionary = [ 143 let data: NSDictionary = [
117 "completed": completed, 144 "completed": completed,
118 - "error": error?.localizedDescription as Any, 145 + "error": error as Any,
  146 + "job": printJob.index,
119 ] 147 ]
120 - channel?.invokeMethod("onCompleted", arguments: data)  
121 -  
122 - renderer = nil  
123 - }  
124 -  
125 - func directPrintPdf(name: String, data: Data, withPrinter printerID: String) {  
126 - let printing = UIPrintInteractionController.isPrintingAvailable  
127 - if !printing {  
128 - let data: NSDictionary = [  
129 - "completed": false,  
130 - "error": "Printing not available",  
131 - ]  
132 - channel?.invokeMethod("onCompleted", arguments: data)  
133 - return  
134 - }  
135 -  
136 - let controller = UIPrintInteractionController.shared  
137 - controller.delegate = self  
138 -  
139 - let printInfo = UIPrintInfo.printInfo()  
140 - printInfo.jobName = name  
141 - printInfo.outputType = .general  
142 - controller.printInfo = printInfo  
143 - controller.printingItem = data  
144 - let printerURL = URL(string: printerID)  
145 -  
146 - if printerURL == nil {  
147 - let data: NSDictionary = [  
148 - "completed": false,  
149 - "error": "Unable to fine printer URL",  
150 - ]  
151 - channel?.invokeMethod("onCompleted", arguments: data)  
152 - return  
153 - }  
154 -  
155 - let printer = UIPrinter(url: printerURL!)  
156 - controller.print(to: printer, completionHandler: completionHandler) 148 + channel.invokeMethod("onCompleted", arguments: data)
157 } 149 }
158 150
159 - func printPdf(_ name: String, data: Data?) {  
160 - let printing = UIPrintInteractionController.isPrintingAvailable  
161 - if !printing {  
162 - let data: NSDictionary = [  
163 - "completed": false,  
164 - "error": "Printing not available",  
165 - ]  
166 - channel?.invokeMethod("onCompleted", arguments: data)  
167 - return  
168 - }  
169 -  
170 - let controller = UIPrintInteractionController.shared  
171 - controller.delegate = self  
172 -  
173 - let printInfo = UIPrintInfo.printInfo()  
174 - printInfo.jobName = name  
175 - printInfo.outputType = .general  
176 - controller.printInfo = printInfo  
177 - renderer = PdfPrintPageRenderer(channel, data: data)  
178 - controller.printPageRenderer = renderer  
179 - controller.present(animated: true, completionHandler: completionHandler)  
180 - }  
181 -  
182 - func writePdf(_ data: Data) {  
183 - renderer?.setDocument(data)  
184 - }  
185 -  
186 - func sharePdf(_ data: FlutterStandardTypedData, withSourceRect rect: CGRect, andName name: String?) {  
187 - let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)  
188 -  
189 - let uuid = CFUUIDCreate(nil)  
190 - assert(uuid != nil)  
191 -  
192 - let uuidStr = CFUUIDCreateString(nil, uuid)  
193 - assert(uuidStr != nil)  
194 -  
195 - var fileURL: URL  
196 - if name == nil {  
197 - fileURL = tmpDirURL.appendingPathComponent("document-\(uuidStr ?? "1" as CFString)").appendingPathExtension("pdf")  
198 - } else {  
199 - fileURL = tmpDirURL.appendingPathComponent(name!)  
200 - }  
201 -  
202 - do {  
203 - try data.data.write(to: fileURL, options: .atomic)  
204 - } catch {  
205 - print("sharePdf error: \(error.localizedDescription)")  
206 - return  
207 - }  
208 -  
209 - let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)  
210 - if UI_USER_INTERFACE_IDIOM() == .pad {  
211 - let controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController  
212 - activityViewController.popoverPresentationController?.sourceView = controller?.view  
213 - activityViewController.popoverPresentationController?.sourceRect = rect  
214 - }  
215 - UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)  
216 - }  
217 -  
218 - func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {  
219 - let viewController = UIApplication.shared.delegate?.window?!.rootViewController  
220 - let wkWebView = WKWebView(frame: viewController!.view.bounds)  
221 - wkWebView.isHidden = true  
222 - wkWebView.tag = 100  
223 - viewController?.view.addSubview(wkWebView)  
224 - wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)  
225 -  
226 - urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in  
227 - // this is workaround for issue with loading local images  
228 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {  
229 - // assign the print formatter to the print page renderer  
230 - let renderer = UIPrintPageRenderer()  
231 - renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)  
232 -  
233 - // assign paperRect and printableRect values  
234 - renderer.setValue(rect, forKey: "paperRect")  
235 - renderer.setValue(margin, forKey: "printableRect")  
236 -  
237 - // create pdf context and draw each page  
238 - let pdfData = NSMutableData()  
239 - UIGraphicsBeginPDFContextToData(pdfData, rect, nil)  
240 -  
241 - for i in 0 ..< renderer.numberOfPages {  
242 - UIGraphicsBeginPDFPage()  
243 - renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())  
244 - }  
245 -  
246 - UIGraphicsEndPDFContext()  
247 -  
248 - if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {  
249 - viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated  
250 -  
251 - // clear WKWebView cache  
252 - if #available(iOS 9.0, *) {  
253 - WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in  
254 - records.forEach { record in  
255 - WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})  
256 - }  
257 - }  
258 - }  
259 - }  
260 -  
261 - // dispose urlObservation  
262 - self.urlObservation = nil  
263 -  
264 - let data = FlutterStandardTypedData(bytes: pdfData as Data)  
265 - self.channel?.invokeMethod("onHtmlRendered", arguments: data)  
266 - }  
267 - }) 151 + /// send html to pdf data result to flutter
  152 + public func onHtmlRendered(printJob: PrintJob, pdfData: Data) {
  153 + let data: NSDictionary = [
  154 + "doc": FlutterStandardTypedData(bytes: pdfData),
  155 + "job": printJob.index,
  156 + ]
  157 + channel.invokeMethod("onHtmlRendered", arguments: data)
268 } 158 }
269 159
270 - func pickPrinter(_ result: @escaping FlutterResult, withSourceRect rect: CGRect) {  
271 - let controller = UIPrinterPickerController(initiallySelectedPrinter: nil)  
272 -  
273 - let pickPrinterCompletionHandler: UIPrinterPickerController.CompletionHandler = {  
274 - (printerPickerController: UIPrinterPickerController, completed: Bool, error: Error?) in  
275 - if !completed, error != nil {  
276 - print("Unable to pick printer: \(error?.localizedDescription ?? "unknown error")")  
277 - result(nil)  
278 - return  
279 - }  
280 -  
281 - if printerPickerController.selectedPrinter == nil {  
282 - result(nil)  
283 - return  
284 - }  
285 -  
286 - let printer = printerPickerController.selectedPrinter!  
287 - let data: NSDictionary = [  
288 - "url": printer.url.absoluteString as Any,  
289 - "name": printer.displayName as Any,  
290 - "model": printer.makeAndModel as Any,  
291 - "location": printer.displayLocation as Any,  
292 - ]  
293 - result(data)  
294 - }  
295 -  
296 - if UI_USER_INTERFACE_IDIOM() == .pad {  
297 - let viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController  
298 - if viewController != nil {  
299 - controller.present(from: rect, in: viewController!.view, animated: true, completionHandler: pickPrinterCompletionHandler)  
300 - return  
301 - }  
302 - }  
303 -  
304 - controller.present(animated: true, completionHandler: pickPrinterCompletionHandler) 160 + /// send html to pdf conversion error to flutter
  161 + public func onHtmlError(printJob: PrintJob, error: String) {
  162 + let data: NSDictionary = [
  163 + "error": error,
  164 + "job": printJob.index,
  165 + ]
  166 + channel.invokeMethod("onHtmlError", arguments: data)
305 } 167 }
306 } 168 }
@@ -27,5 +27,8 @@ import 'package:pdf/pdf.dart'; @@ -27,5 +27,8 @@ import 'package:pdf/pdf.dart';
27 import 'package:pdf/widgets.dart'; 27 import 'package:pdf/widgets.dart';
28 28
29 part 'src/asset_utils.dart'; 29 part 'src/asset_utils.dart';
  30 +part 'src/print_job.dart';
  31 +part 'src/printer.dart';
30 part 'src/printing.dart'; 32 part 'src/printing.dart';
  33 +part 'src/printing_info.dart';
31 part 'src/widgets.dart'; 34 part 'src/widgets.dart';
  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 +part of printing;
  18 +
  19 +class _PrintJob {
  20 + _PrintJob({
  21 + this.onLayout,
  22 + this.onHtmlRendered,
  23 + this.onCompleted,
  24 + });
  25 +
  26 + final LayoutCallback onLayout;
  27 + final Completer<List<int>> onHtmlRendered;
  28 + final Completer<bool> onCompleted;
  29 +
  30 + int index;
  31 +}
  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 +part of printing;
  18 +
  19 +@immutable
  20 +class Printer {
  21 + const Printer({
  22 + @required this.url,
  23 + this.name,
  24 + this.model,
  25 + this.location,
  26 + }) : assert(url != null);
  27 +
  28 + final String url;
  29 + final String name;
  30 + final String model;
  31 + final String location;
  32 +
  33 + @override
  34 + String toString() => name ?? url;
  35 +}
@@ -18,74 +18,65 @@ part of printing; @@ -18,74 +18,65 @@ part of printing;
18 18
19 typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format); 19 typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format);
20 20
21 -@immutable  
22 -class Printer {  
23 - const Printer({  
24 - @required this.url,  
25 - this.name,  
26 - this.model,  
27 - this.location,  
28 - }) : assert(url != null);  
29 -  
30 - final String url;  
31 - final String name;  
32 - final String model;  
33 - final String location;  
34 -  
35 - @override  
36 - String toString() => name ?? url;  
37 -}  
38 -  
39 mixin Printing { 21 mixin Printing {
40 static const MethodChannel _channel = MethodChannel('net.nfet.printing'); 22 static const MethodChannel _channel = MethodChannel('net.nfet.printing');
41 - static LayoutCallback _onLayout;  
42 - static Completer<List<int>> _onHtmlRendered;  
43 - static Completer<bool> _onCompleted; 23 + static final Map<int, _PrintJob> _printJobs = <int, _PrintJob>{};
  24 + static int _jobIndex = 0;
44 25
45 /// Callbacks from platform plugins 26 /// Callbacks from platform plugins
46 - static Future<void> _handleMethod(MethodCall call) async { 27 + static Future<dynamic> _handleMethod(MethodCall call) async {
47 switch (call.method) { 28 switch (call.method) {
48 case 'onLayout': 29 case 'onLayout':
  30 + final _PrintJob job = _printJobs[call.arguments['job']];
49 try { 31 try {
50 - final List<int> bytes = await _onLayout(PdfPageFormat( 32 + final PdfPageFormat format = PdfPageFormat(
51 call.arguments['width'], 33 call.arguments['width'],
52 call.arguments['height'], 34 call.arguments['height'],
53 marginLeft: call.arguments['marginLeft'], 35 marginLeft: call.arguments['marginLeft'],
54 marginTop: call.arguments['marginTop'], 36 marginTop: call.arguments['marginTop'],
55 marginRight: call.arguments['marginRight'], 37 marginRight: call.arguments['marginRight'],
56 marginBottom: call.arguments['marginBottom'], 38 marginBottom: call.arguments['marginBottom'],
57 - )); 39 + );
  40 +
  41 + final List<int> bytes = await job.onLayout(format);
  42 +
58 if (bytes == null) { 43 if (bytes == null) {
59 - await _channel.invokeMethod<void>('cancelJob', <String, dynamic>{});  
60 - break; 44 + return false;
61 } 45 }
62 - final Map<String, dynamic> params = <String, dynamic>{  
63 - 'doc': Uint8List.fromList(bytes),  
64 - };  
65 - await _channel.invokeMethod<void>('writePdf', params); 46 +
  47 + return Uint8List.fromList(bytes);
66 } catch (e) { 48 } catch (e) {
67 print('Unable to print: $e'); 49 print('Unable to print: $e');
68 - await _channel.invokeMethod<void>('cancelJob', <String, dynamic>{}); 50 + return false;
69 } 51 }
70 break; 52 break;
71 case 'onCompleted': 53 case 'onCompleted':
72 final bool completed = call.arguments['completed']; 54 final bool completed = call.arguments['completed'];
73 final String error = call.arguments['error']; 55 final String error = call.arguments['error'];
  56 + final _PrintJob job = _printJobs[call.arguments['job']];
74 if (completed == false && error != null) { 57 if (completed == false && error != null) {
75 - _onCompleted.completeError(error); 58 + job.onCompleted.completeError(error);
76 } else { 59 } else {
77 - _onCompleted.complete(completed); 60 + job.onCompleted.complete(completed);
78 } 61 }
79 break; 62 break;
80 case 'onHtmlRendered': 63 case 'onHtmlRendered':
81 - _onHtmlRendered.complete(call.arguments); 64 + final _PrintJob job = _printJobs[call.arguments['job']];
  65 + job.onHtmlRendered.complete(call.arguments['doc']);
82 break; 66 break;
83 case 'onHtmlError': 67 case 'onHtmlError':
84 - _onHtmlRendered.completeError(call.arguments); 68 + final _PrintJob job = _printJobs[call.arguments['job']];
  69 + job.onHtmlRendered.completeError(call.arguments['error']);
85 break; 70 break;
86 } 71 }
87 } 72 }
88 73
  74 + static _PrintJob _newPrintJob(_PrintJob job) {
  75 + job.index = _jobIndex++;
  76 + _printJobs[job.index] = job;
  77 + return job;
  78 + }
  79 +
89 /// Prints a Pdf document to a local printer using the platform UI 80 /// Prints a Pdf document to a local printer using the platform UI
90 /// the Pdf document is re-built in a [LayoutCallback] each time the 81 /// the Pdf document is re-built in a [LayoutCallback] each time the
91 /// user changes a setting like the page format or orientation. 82 /// user changes a setting like the page format or orientation.
@@ -98,24 +89,33 @@ mixin Printing { @@ -98,24 +89,33 @@ mixin Printing {
98 String name = 'Document', 89 String name = 'Document',
99 PdfPageFormat format = PdfPageFormat.standard, 90 PdfPageFormat format = PdfPageFormat.standard,
100 }) async { 91 }) async {
101 - _onCompleted = Completer<bool>();  
102 - _onLayout = onLayout;  
103 _channel.setMethodCallHandler(_handleMethod); 92 _channel.setMethodCallHandler(_handleMethod);
104 - final Map<String, dynamic> params = <String, dynamic>{'name': name}; 93 +
  94 + final _PrintJob job = _newPrintJob(_PrintJob(
  95 + onCompleted: Completer<bool>(),
  96 + onLayout: onLayout,
  97 + ));
  98 +
  99 + final Map<String, dynamic> params = <String, dynamic>{
  100 + 'name': name,
  101 + 'job': job.index,
  102 + 'width': format.width,
  103 + 'height': format.height,
  104 + 'marginLeft': format.marginLeft,
  105 + 'marginTop': format.marginTop,
  106 + 'marginRight': format.marginRight,
  107 + 'marginBottom': format.marginBottom,
  108 + };
  109 +
  110 + await _channel.invokeMethod<int>('printPdf', params);
  111 + bool result = false;
105 try { 112 try {
106 - final Map<dynamic, dynamic> info = await printingInfo();  
107 - if (int.parse(info['iosVersion'].toString().split('.').first) >= 13) {  
108 - final List<int> bytes = await onLayout(format);  
109 - if (bytes == null) {  
110 - return false;  
111 - }  
112 - params['doc'] = Uint8List.fromList(bytes);  
113 - } 113 + result = await job.onCompleted.future;
114 } catch (e) { 114 } catch (e) {
115 - e.toString(); 115 + print('Document not printed: $e');
116 } 116 }
117 - await _channel.invokeMethod<int>('printPdf', params);  
118 - return _onCompleted.future; 117 + _printJobs.remove(job.index);
  118 + return result;
119 } 119 }
120 120
121 static Future<Map<dynamic, dynamic>> printingInfo() async { 121 static Future<Map<dynamic, dynamic>> printingInfo() async {
@@ -159,19 +159,31 @@ mixin Printing { @@ -159,19 +159,31 @@ mixin Printing {
159 String name = 'Document', 159 String name = 'Document',
160 PdfPageFormat format = PdfPageFormat.standard, 160 PdfPageFormat format = PdfPageFormat.standard,
161 }) async { 161 }) async {
162 - _onCompleted = Completer<bool>(); 162 + if (printer == null) {
  163 + return false;
  164 + }
  165 +
163 _channel.setMethodCallHandler(_handleMethod); 166 _channel.setMethodCallHandler(_handleMethod);
  167 +
  168 + final _PrintJob job = _newPrintJob(_PrintJob(
  169 + onCompleted: Completer<bool>(),
  170 + ));
  171 +
164 final List<int> bytes = await onLayout(format); 172 final List<int> bytes = await onLayout(format);
165 if (bytes == null) { 173 if (bytes == null) {
166 return false; 174 return false;
167 } 175 }
  176 +
168 final Map<String, dynamic> params = <String, dynamic>{ 177 final Map<String, dynamic> params = <String, dynamic>{
169 'name': name, 178 'name': name,
170 'printer': printer.url, 179 'printer': printer.url,
171 'doc': Uint8List.fromList(bytes), 180 'doc': Uint8List.fromList(bytes),
  181 + 'job': job.index,
172 }; 182 };
173 await _channel.invokeMethod<int>('directPrintPdf', params); 183 await _channel.invokeMethod<int>('directPrintPdf', params);
174 - return _onCompleted.future; 184 + final bool result = await job.onCompleted.future;
  185 + _printJobs.remove(job.index);
  186 + return result;
175 } 187 }
176 188
177 /// Prints a [PdfDocument] or a pdf stream to a local printer using the platform UI 189 /// Prints a [PdfDocument] or a pdf stream to a local printer using the platform UI
@@ -192,11 +204,12 @@ mixin Printing { @@ -192,11 +204,12 @@ mixin Printing {
192 static Future<void> sharePdf({ 204 static Future<void> sharePdf({
193 @Deprecated('use bytes with document.save()') PdfDocument document, 205 @Deprecated('use bytes with document.save()') PdfDocument document,
194 List<int> bytes, 206 List<int> bytes,
195 - String filename, 207 + String filename = 'document.pdf',
196 Rect bounds, 208 Rect bounds,
197 }) async { 209 }) async {
198 assert(document != null || bytes != null); 210 assert(document != null || bytes != null);
199 assert(!(document == null && bytes == null)); 211 assert(!(document == null && bytes == null));
  212 + assert(filename != null);
200 213
201 if (document != null) { 214 if (document != null) {
202 bytes = document.save(); 215 bytes = document.save();
@@ -220,6 +233,12 @@ mixin Printing { @@ -220,6 +233,12 @@ mixin Printing {
220 {@required String html, 233 {@required String html,
221 String baseUrl, 234 String baseUrl,
222 PdfPageFormat format = PdfPageFormat.a4}) async { 235 PdfPageFormat format = PdfPageFormat.a4}) async {
  236 + _channel.setMethodCallHandler(_handleMethod);
  237 +
  238 + final _PrintJob job = _newPrintJob(_PrintJob(
  239 + onHtmlRendered: Completer<List<int>>(),
  240 + ));
  241 +
223 final Map<String, dynamic> params = <String, dynamic>{ 242 final Map<String, dynamic> params = <String, dynamic>{
224 'html': html, 243 'html': html,
225 'baseUrl': baseUrl, 244 'baseUrl': baseUrl,
@@ -229,11 +248,29 @@ mixin Printing { @@ -229,11 +248,29 @@ mixin Printing {
229 'marginTop': format.marginTop, 248 'marginTop': format.marginTop,
230 'marginRight': format.marginRight, 249 'marginRight': format.marginRight,
231 'marginBottom': format.marginBottom, 250 'marginBottom': format.marginBottom,
  251 + 'job': job.index,
232 }; 252 };
233 253
234 - _channel.setMethodCallHandler(_handleMethod);  
235 - _onHtmlRendered = Completer<List<int>>();  
236 await _channel.invokeMethod<void>('convertHtml', params); 254 await _channel.invokeMethod<void>('convertHtml', params);
237 - return _onHtmlRendered.future; 255 + final List<int> result = await job.onHtmlRendered.future;
  256 + _printJobs.remove(job.index);
  257 + return result;
  258 + }
  259 +
  260 + static Future<PrintingInfo> info() async {
  261 + _channel.setMethodCallHandler(_handleMethod);
  262 + Map<dynamic, dynamic> result;
  263 +
  264 + try {
  265 + result = await _channel.invokeMethod(
  266 + 'printingInfo',
  267 + <String, dynamic>{},
  268 + );
  269 + } catch (e) {
  270 + print('Error getting printing info: $e');
  271 + return PrintingInfo.unavailable;
  272 + }
  273 +
  274 + return PrintingInfo.fromMap(result);
238 } 275 }
239 } 276 }
  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 +part of printing;
  18 +
  19 +class PrintingInfo {
  20 + factory PrintingInfo.fromMap(Map<dynamic, dynamic> map) => PrintingInfo._(
  21 + directPrint: map['directPrint'] ?? false,
  22 + dynamicLayout: map['dynamicLayout'] ?? false,
  23 + canPrint: map['canPrint'],
  24 + canConvertHtml: map['canConvertHtml'],
  25 + canShare: map['canShare'],
  26 + );
  27 +
  28 + const PrintingInfo._({
  29 + this.directPrint = false,
  30 + this.dynamicLayout = false,
  31 + this.canPrint = false,
  32 + this.canConvertHtml = false,
  33 + this.canShare = false,
  34 + }) : assert(directPrint != null),
  35 + assert(dynamicLayout != null),
  36 + assert(canPrint != null),
  37 + assert(canConvertHtml != null),
  38 + assert(canShare != null);
  39 +
  40 + static const PrintingInfo unavailable = PrintingInfo._();
  41 +
  42 + final bool directPrint;
  43 + final bool dynamicLayout;
  44 + final bool canPrint;
  45 + final bool canConvertHtml;
  46 + final bool canShare;
  47 +}