Showing
16 changed files
with
963 additions
and
640 deletions
| @@ -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 | - | 48 | + Activity activity = registrar.activity(); |
| 79 | if (activity == null) { | 49 | 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(); | 50 | + return; // We can't print without an activity |
| 110 | } | 51 | } |
| 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 | 52 | ||
| 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); | 53 | + final MethodChannel channel = new MethodChannel(registrar.messenger(), "net.nfet.printing"); |
| 54 | + channel.setMethodCallHandler(new PrintingPlugin(activity, channel)); | ||
| 144 | } | 55 | } |
| 145 | 56 | ||
| 146 | @Override | 57 | @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); | ||
| 171 | - } | ||
| 172 | - | ||
| 173 | - printJob = null; | ||
| 174 | - mPdfDocument = null; | ||
| 175 | - } | ||
| 176 | - | ||
| 177 | - @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); | ||
| 186 | - 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); | 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); | ||
| 200 | break; | 76 | 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); |
| 122 | + break; | ||
| 123 | + } | ||
| 124 | + case "printingInfo": { | ||
| 125 | + result.success(PrintingJob.printingInfo()); | ||
| 231 | break; | 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 | - } | ||
| 248 | - | ||
| 249 | - FileOutputStream stream = new FileOutputStream(shareFile); | ||
| 250 | - stream.write(data); | ||
| 251 | - stream.close(); | 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); | ||
| 252 | 140 | ||
| 253 | - Uri apkURI = FileProvider.getUriForFile(activity, | ||
| 254 | - activity.getApplicationContext().getPackageName() + ".flutter.printing", | ||
| 255 | - shareFile); | 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); | ||
| 256 | 146 | ||
| 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(); | 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(); | ||
| 267 | } | 154 | } |
| 268 | } | 155 | } |
| 269 | 156 | ||
| 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()); | ||
| 273 | - | ||
| 274 | - webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null); | ||
| 275 | - | ||
| 276 | - webView.setWebViewClient(new WebViewClient() { | ||
| 277 | @Override | 157 | @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(); | ||
| 288 | - | ||
| 289 | - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||
| 290 | - final PrintDocumentAdapter adapter = | ||
| 291 | - webView.createPrintDocumentAdapter("printing"); | ||
| 292 | - | ||
| 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 | - } | 158 | + public void error(String errorCode, String errorMessage, Object errorDetails) { |
| 159 | + printJob.cancelJob(); | ||
| 302 | } | 160 | } |
| 303 | 161 | ||
| 304 | @Override | 162 | @Override |
| 305 | - public void onError(String message) { | ||
| 306 | - channel.invokeMethod("onHtmlError", message); | 163 | + public void notImplemented() { |
| 164 | + printJob.cancelJob(); | ||
| 307 | } | 165 | } |
| 308 | }); | 166 | }); |
| 309 | } | 167 | } |
| 168 | + | ||
| 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); | ||
| 173 | + | ||
| 174 | + args.put("error", error); | ||
| 175 | + args.put("job", printJob.index); | ||
| 176 | + | ||
| 177 | + channel.invokeMethod("onCompleted", args); | ||
| 310 | } | 178 | } |
| 179 | + | ||
| 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); | ||
| 185 | + | ||
| 186 | + channel.invokeMethod("onHtmlRendered", args); | ||
| 311 | } | 187 | } |
| 312 | - }); | 188 | + |
| 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 ...'); |
| 166 | + final bool result = | ||
| 139 | await Printing.layoutPdf(onLayout: (PdfPageFormat format) async { | 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 ...'); |
| 177 | + final bool result = | ||
| 147 | await Printing.layoutPdf(onLayout: (PdfPageFormat format) async { | 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, | 213 | key: pickWidget, |
| 179 | child: const Text('Pick Printer'), | 214 | child: const Text('Pick Printer'), |
| 180 | - onPressed: _pickPrinter), | 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 | 221 | child: Text(selectedPrinter == null |
| 184 | ? 'Direct Print' | 222 | ? 'Direct Print' |
| 185 | : 'Print to $selectedPrinter'), | 223 | : 'Print to $selectedPrinter'), |
| 186 | onPressed: | 224 | onPressed: |
| 187 | - selectedPrinter != null ? _directPrintPdf : null), | 225 | + selectedPrinter != null ? _directPrintPdf : null, |
| 226 | + ), | ||
| 188 | ], | 227 | ], |
| 189 | ), | 228 | ), |
| 190 | RaisedButton( | 229 | RaisedButton( |
| 191 | key: shareWidget, | 230 | key: shareWidget, |
| 192 | child: const Text('Share Document'), | 231 | child: const Text('Share Document'), |
| 193 | - onPressed: _sharePdf), | 232 | + onPressed: printingInfo?.canShare ?? false ? _sharePdf : null, |
| 233 | + ), | ||
| 194 | RaisedButton( | 234 | RaisedButton( |
| 195 | child: const Text('Print Screenshot'), | 235 | child: const Text('Print Screenshot'), |
| 196 | - onPressed: _printScreen), | 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'), | 247 | child: const Text('Print Markdown'), |
| 203 | - onPressed: _printMarkdown), | 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, |
printing/ios/Assets/.gitkeep
deleted
100644 → 0
| 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 | -} |
printing/ios/Classes/PrintJob.swift
0 → 100644
| 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 | + 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 | ||
| 67 | ) | 77 | ) |
| 68 | - } | ||
| 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 | - } | ||
| 115 | - | ||
| 116 | - let data: NSDictionary = [ | ||
| 117 | - "completed": completed, | ||
| 118 | - "error": error?.localizedDescription as Any, | ||
| 119 | - ] | ||
| 120 | - channel?.invokeMethod("onCompleted", arguments: data) | 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] | ||
| 121 | 130 | ||
| 122 | - renderer = nil | 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) | ||
| 123 | } | 137 | } |
| 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 | 138 | + }) |
| 134 | } | 139 | } |
| 135 | 140 | ||
| 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 { | 141 | + /// send completion status to flutter |
| 142 | + public func onCompleted(printJob: PrintJob, completed: Bool, error: NSString?) { | ||
| 147 | let data: NSDictionary = [ | 143 | let data: NSDictionary = [ |
| 148 | - "completed": false, | ||
| 149 | - "error": "Unable to fine printer URL", | 144 | + "completed": completed, |
| 145 | + "error": error as Any, | ||
| 146 | + "job": printJob.index, | ||
| 150 | ] | 147 | ] |
| 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 { | 151 | + /// send html to pdf data result to flutter |
| 152 | + public func onHtmlRendered(printJob: PrintJob, pdfData: Data) { | ||
| 162 | let data: NSDictionary = [ | 153 | let data: NSDictionary = [ |
| 163 | - "completed": false, | ||
| 164 | - "error": "Printing not available", | 154 | + "doc": FlutterStandardTypedData(bytes: pdfData), |
| 155 | + "job": printJob.index, | ||
| 165 | ] | 156 | ] |
| 166 | - channel?.invokeMethod("onCompleted", arguments: data) | ||
| 167 | - return | 157 | + channel.invokeMethod("onHtmlRendered", arguments: data) |
| 168 | } | 158 | } |
| 169 | 159 | ||
| 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 | - }) | ||
| 268 | - } | ||
| 269 | - | ||
| 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! | 160 | + /// send html to pdf conversion error to flutter |
| 161 | + public func onHtmlError(printJob: PrintJob, error: String) { | ||
| 287 | let data: NSDictionary = [ | 162 | 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, | 163 | + "error": error, |
| 164 | + "job": printJob.index, | ||
| 292 | ] | 165 | ] |
| 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) | 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'; |
printing/lib/src/print_job.dart
0 → 100644
| 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 | +} |
printing/lib/src/printer.dart
0 → 100644
| 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 | } |
printing/lib/src/printing_info.dart
0 → 100644
| 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 | +} |
-
Please register or login to post a comment