David PHAM-VAN

Improve native code

... ... @@ -3,6 +3,8 @@
## 2.2.0
- Simplify iOS code
- Improve native code
- Add Printing.info()
## 2.1.9
... ...
... ... @@ -16,9 +16,10 @@
package android.print;
import android.app.Activity;
import android.content.Context;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
... ... @@ -27,17 +28,16 @@ import java.io.IOException;
import java.io.InputStream;
public class PdfConvert {
public static void print(final Activity activity, final PrintDocumentAdapter adapter,
public static void print(final Context context, final PrintDocumentAdapter adapter,
final PrintAttributes attributes, final Result result) {
adapter.onLayout(null, attributes, null, new PrintDocumentAdapter.LayoutResultCallback() {
@Override
public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
File outputDir = activity.getCacheDir();
File outputFile = null;
File outputDir = context.getCacheDir();
File outputFile;
try {
outputFile = File.createTempFile("printing", "pdf", outputDir);
} catch (IOException e) {
outputFile.delete();
result.onError(e.getMessage());
return;
}
... ... @@ -54,16 +54,22 @@ public class PdfConvert {
super.onWriteFinished(pages);
if (pages.length == 0) {
finalOutputFile.delete();
if (!finalOutputFile.delete()) {
Log.e("PDF", "Unable to delete temporary file");
}
result.onError("No page created");
}
result.onSuccess(finalOutputFile);
finalOutputFile.delete();
if (!finalOutputFile.delete()) {
Log.e("PDF", "Unable to delete temporary file");
}
}
});
} catch (FileNotFoundException e) {
outputFile.delete();
if (!outputFile.delete()) {
Log.e("PDF", "Unable to delete temporary file");
}
result.onError(e.getMessage());
}
}
... ... @@ -72,17 +78,10 @@ public class PdfConvert {
public static byte[] readFile(File file) throws IOException {
byte[] buffer = new byte[(int) file.length()];
InputStream ios = null;
try {
ios = new FileInputStream(file);
try (InputStream ios = new FileInputStream(file)) {
if (ios.read(buffer) == -1) {
throw new IOException("EOF reached while trying to read the whole file");
}
} finally {
try {
if (ios != null) ios.close();
} catch (IOException e) {
}
}
return buffer;
}
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.nfet.flutter.printing;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PdfConvert;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import android.print.PrintJob;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
/**
* PrintJob
*/
public class PrintingJob extends PrintDocumentAdapter {
private static PrintManager printManager;
private final Context context;
private final PrintingPlugin printing;
private PrintJob printJob;
private byte[] documentData;
private String jobName;
private LayoutResultCallback callback;
int index;
PrintingJob(Context context, PrintingPlugin printing, int index) {
this.context = context;
this.printing = printing;
this.index = index;
printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
}
static HashMap<String, Object> printingInfo() {
HashMap<String, Object> result = new HashMap<>();
result.put("directPrint", false);
result.put("dynamicLayout", true);
result.put("canPrint", true);
result.put("canConvertHtml", true);
result.put("canShare", true);
return result;
}
@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor parcelFileDescriptor,
CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback) {
OutputStream output = null;
try {
output = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
output.write(documentData, 0, documentData.length);
writeResultCallback.onWriteFinished(new PageRange[] {PageRange.ALL_PAGES});
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (output != null) {
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
// Respond to cancellation request
if (cancellationSignal.isCanceled()) {
callback.onLayoutCancelled();
return;
}
this.callback = callback;
PrintAttributes.MediaSize size = newAttributes.getMediaSize();
PrintAttributes.Margins margins = newAttributes.getMinMargins();
assert size != null;
assert margins != null;
printing.onLayout(this, size.getWidthMils() * 72.0 / 1000.0,
size.getHeightMils() * 72.0 / 1000.0, margins.getLeftMils() * 72.0 / 1000.0,
margins.getTopMils() * 72.0 / 1000.0, margins.getRightMils() * 72.0 / 1000.0,
margins.getBottomMils() * 72.0 / 1000.0);
}
@Override
public void onFinish() {
try {
while (true) {
int state = printJob.getInfo().getState();
if (state == PrintJobInfo.STATE_COMPLETED) {
printing.onCompleted(this, true, "");
break;
} else if (state == PrintJobInfo.STATE_CANCELED) {
printing.onCompleted(this, false, "User canceled");
break;
}
Thread.sleep(200);
}
} catch (Exception e) {
printing.onCompleted(this, printJob != null && printJob.isCompleted(), e.getMessage());
}
printJob = null;
}
void printPdf(@NonNull String name, Double width, Double height, Double marginLeft,
Double marginTop, Double marginRight, Double marginBottom) {
jobName = name;
printJob = printManager.print(name, this, null);
}
void cancelJob() {
if (callback != null) callback.onLayoutCancelled();
if (printJob != null) printJob.cancel();
}
static void sharePdf(final Context context, final byte[] data, final String name) {
assert name != null;
try {
final File externalFilesDirectory =
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File shareFile = new File(externalFilesDirectory, name);
FileOutputStream stream = new FileOutputStream(shareFile);
stream.write(data);
stream.close();
Uri apkURI = FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".flutter.printing",
shareFile);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, apkURI);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent chooserIntent = Intent.createChooser(shareIntent, null);
context.startActivity(chooserIntent);
shareFile.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
}
}
void convertHtml(final String data, final PrintAttributes.MediaSize size,
final PrintAttributes.Margins margins, final String baseUrl) {
final WebView webView = new WebView(context.getApplicationContext());
webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
PrintAttributes attributes =
new PrintAttributes.Builder()
.setMediaSize(size)
.setResolution(
new PrintAttributes.Resolution("pdf", "pdf", 600, 600))
.setMinMargins(margins)
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final PrintDocumentAdapter adapter =
webView.createPrintDocumentAdapter("printing");
PdfConvert.print(context, adapter, attributes, new PdfConvert.Result() {
@Override
public void onSuccess(File file) {
try {
byte[] fileContent = PdfConvert.readFile(file);
printing.onHtmlRendered(PrintingJob.this, fileContent);
} catch (IOException e) {
onError(e.getMessage());
}
}
@Override
public void onError(String message) {
printing.onHtmlError(PrintingJob.this, message);
}
});
}
}
}
});
}
void setDocument(byte[] data) {
documentData = data;
PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName + ".pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.build();
// Content layout reflow is complete
callback.onLayoutFinished(info, true);
}
}
... ...
... ... @@ -17,32 +17,10 @@
package net.nfet.flutter.printing;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PdfConvert;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import android.print.PrintJob;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.print.pdf.PrintedPdfDocument;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.core.content.FileProvider;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import io.flutter.plugin.common.MethodCall;
... ... @@ -54,17 +32,11 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
/**
* PrintingPlugin
*/
public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHandler {
private static PrintManager printManager;
public class PrintingPlugin implements MethodCallHandler {
private final Activity activity;
private final MethodChannel channel;
private PrintedPdfDocument mPdfDocument;
private PrintJob printJob;
private byte[] documentData;
private String jobName;
private LayoutResultCallback callback;
private PrintingPlugin(Activity activity, MethodChannel channel) {
private PrintingPlugin(@NonNull Activity activity, @NonNull MethodChannel channel) {
this.activity = activity;
this.channel = channel;
}
... ... @@ -73,147 +45,66 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
try {
final Activity activity = registrar.activity();
if (activity == null) {
return;
}
final MethodChannel channel =
new MethodChannel(registrar.messenger(), "net.nfet.printing");
final PrintingPlugin plugin = new PrintingPlugin(activity, channel);
channel.setMethodCallHandler(plugin);
if (printManager == null) {
printManager = (PrintManager) activity.getSystemService(Context.PRINT_SERVICE);
}
} catch (Exception e) {
Log.e("PrintingPlugin", "Registration failed", e);
}
}
@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor parcelFileDescriptor,
CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback) {
OutputStream output = null;
try {
output = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
output.write(documentData, 0, documentData.length);
writeResultCallback.onWriteFinished(new PageRange[] {PageRange.ALL_PAGES});
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (output != null) {
output.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
// Create a new PdfDocument with the requested page attributes
mPdfDocument = new PrintedPdfDocument(activity, newAttributes);
// Respond to cancellation request
if (cancellationSignal.isCanceled()) {
callback.onLayoutCancelled();
return;
}
this.callback = callback;
HashMap<String, Double> args = new HashMap<>();
PrintAttributes.MediaSize size = newAttributes.getMediaSize();
args.put("width", size.getWidthMils() * 72.0 / 1000.0);
args.put("height", size.getHeightMils() * 72.0 / 1000.0);
PrintAttributes.Margins margins = newAttributes.getMinMargins();
args.put("marginLeft", margins.getLeftMils() * 72.0 / 1000.0);
args.put("marginTop", margins.getTopMils() * 72.0 / 1000.0);
args.put("marginRight", margins.getRightMils() * 72.0 / 1000.0);
args.put("marginBottom", margins.getBottomMils() * 72.0 / 1000.0);
channel.invokeMethod("onLayout", args);
}
@Override
public void onFinish() {
try {
while (true) {
int state = printJob.getInfo().getState();
if (state == PrintJobInfo.STATE_COMPLETED) {
HashMap<String, Object> args = new HashMap<>();
args.put("completed", true);
channel.invokeMethod("onCompleted", args);
break;
} else if (state == PrintJobInfo.STATE_CANCELED) {
HashMap<String, Object> args = new HashMap<>();
args.put("completed", false);
channel.invokeMethod("onCompleted", args);
break;
}
Thread.sleep(200);
}
} catch (Exception e) {
HashMap<String, Object> args = new HashMap<>();
args.put("completed", printJob != null && printJob.isCompleted());
args.put("error", e.getMessage());
channel.invokeMethod("onCompleted", args);
Activity activity = registrar.activity();
if (activity == null) {
return; // We can't print without an activity
}
printJob = null;
mPdfDocument = null;
final MethodChannel channel = new MethodChannel(registrar.messenger(), "net.nfet.printing");
channel.setMethodCallHandler(new PrintingPlugin(activity, channel));
}
@Override
public void onMethodCall(MethodCall call, Result result) {
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "printPdf":
jobName =
call.argument("name") == null ? "Document" : (String) call.argument("name");
assert jobName != null;
printJob = printManager.print(jobName, this, null);
result.success(0);
case "printPdf": {
final String name = call.argument("name");
final Double width = call.argument("width");
final Double height = call.argument("height");
final Double marginLeft = call.argument("marginLeft");
final Double marginTop = call.argument("marginTop");
final Double marginRight = call.argument("marginRight");
final Double marginBottom = call.argument("marginBottom");
final PrintingJob printJob =
new PrintingJob(activity, this, (int) call.argument("job"));
assert name != null;
printJob.printPdf(
name, width, height, marginLeft, marginTop, marginRight, marginBottom);
result.success(1);
break;
case "writePdf":
documentData = (byte[]) call.argument("doc");
// Return print information to print framework
PrintDocumentInfo info =
new PrintDocumentInfo.Builder(jobName + ".pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.build();
// Content layout reflow is complete
callback.onLayoutFinished(info, true);
result.success(0);
break;
case "cancelJob":
if (callback != null) callback.onLayoutCancelled();
if (printJob != null) printJob.cancel();
result.success(0);
}
case "cancelJob": {
final PrintingJob printJob =
new PrintingJob(activity, this, (int) call.argument("job"));
printJob.cancelJob();
result.success(1);
break;
case "sharePdf":
sharePdf((byte[]) call.argument("doc"), (String) call.argument("name"));
result.success(0);
}
case "sharePdf": {
final byte[] document = call.argument("doc");
final String name = call.argument("name");
PrintingJob.sharePdf(activity, document, name);
result.success(1);
break;
case "convertHtml":
double width = (double) call.argument("width");
double height = (double) call.argument("height");
double marginLeft = (double) call.argument("marginLeft");
double marginTop = (double) call.argument("marginTop");
double marginRight = (double) call.argument("marginRight");
double marginBottom = (double) call.argument("marginBottom");
}
case "convertHtml": {
Double width = call.argument("width");
Double height = call.argument("height");
Double marginLeft = call.argument("marginLeft");
Double marginTop = call.argument("marginTop");
Double marginRight = call.argument("marginRight");
Double marginBottom = call.argument("marginBottom");
final PrintingJob printJob =
new PrintingJob(activity, this, (int) call.argument("job"));
assert width != null;
assert height != null;
assert marginLeft != null;
assert marginTop != null;
assert marginRight != null;
assert marginBottom != null;
PrintAttributes.Margins margins =
new PrintAttributes.Margins(Double.valueOf(marginLeft * 1000.0).intValue(),
... ... @@ -225,90 +116,82 @@ public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHa
"Provided size", Double.valueOf(width * 1000.0 / 72.0).intValue(),
Double.valueOf(height * 1000.0 / 72.0).intValue());
convertHtml((String) call.argument("html"), size, margins,
printJob.convertHtml((String) call.argument("html"), size, margins,
(String) call.argument("baseUrl"));
result.success(0);
result.success(1);
break;
}
case "printingInfo": {
result.success(PrintingJob.printingInfo());
break;
}
default:
result.notImplemented();
break;
}
}
private void sharePdf(byte[] data, String name) {
try {
final File externalFilesDirectory =
activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File shareFile;
if (name == null) {
shareFile = File.createTempFile("document-", ".pdf", externalFilesDirectory);
} else {
shareFile = new File(externalFilesDirectory, name);
}
/// Request the Pdf document from flutter
void onLayout(final PrintingJob printJob, Double width, double height, double marginLeft,
double marginTop, double marginRight, double marginBottom) {
HashMap<String, Object> args = new HashMap<>();
args.put("width", width);
args.put("height", height);
FileOutputStream stream = new FileOutputStream(shareFile);
stream.write(data);
stream.close();
args.put("marginLeft", marginLeft);
args.put("marginTop", marginTop);
args.put("marginRight", marginRight);
args.put("marginBottom", marginBottom);
args.put("job", printJob.index);
channel.invokeMethod("onLayout", args, new Result() {
@Override
public void success(Object result) {
if (result instanceof byte[]) {
printJob.setDocument((byte[]) result);
} else {
printJob.cancelJob();
}
}
Uri apkURI = FileProvider.getUriForFile(activity,
activity.getApplicationContext().getPackageName() + ".flutter.printing",
shareFile);
@Override
public void error(String errorCode, String errorMessage, Object errorDetails) {
printJob.cancelJob();
}
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, apkURI);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent chooserIntent = Intent.createChooser(shareIntent, null);
activity.startActivity(chooserIntent);
shareFile.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
}
@Override
public void notImplemented() {
printJob.cancelJob();
}
});
}
private void convertHtml(String data, final PrintAttributes.MediaSize size,
final PrintAttributes.Margins margins, String baseUrl) {
final WebView webView = new WebView(activity.getApplicationContext());
/// send completion status to flutter
void onCompleted(PrintingJob printJob, boolean completed, String error) {
HashMap<String, Object> args = new HashMap<>();
args.put("completed", completed);
webView.loadDataWithBaseURL(baseUrl, data, "text/HTML", "UTF-8", null);
args.put("error", error);
args.put("job", printJob.index);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
PrintAttributes attributes =
new PrintAttributes.Builder()
.setMediaSize(size)
.setResolution(
new PrintAttributes.Resolution("pdf", "pdf", 600, 600))
.setMinMargins(margins)
.build();
channel.invokeMethod("onCompleted", args);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final PrintDocumentAdapter adapter =
webView.createPrintDocumentAdapter("printing");
/// send html to pdf data result to flutter
void onHtmlRendered(PrintingJob printJob, byte[] pdfData) {
HashMap<String, Object> args = new HashMap<>();
args.put("doc", pdfData);
args.put("job", printJob.index);
PdfConvert.print(activity, adapter, attributes, new PdfConvert.Result() {
@Override
public void onSuccess(File file) {
try {
byte[] fileContent = PdfConvert.readFile(file);
channel.invokeMethod("onHtmlRendered", fileContent);
} catch (IOException e) {
onError(e.getMessage());
}
}
channel.invokeMethod("onHtmlRendered", args);
}
@Override
public void onError(String message) {
channel.invokeMethod("onHtmlError", message);
}
});
}
}
}
});
/// send html to pdf conversion error to flutter
void onHtmlError(PrintingJob printJob, String error) {
HashMap<String, Object> args = new HashMap<>();
args.put("error", error);
args.put("job", printJob.index);
channel.invokeMethod("onHtmlError", args);
}
}
... ...
... ... @@ -68,6 +68,9 @@
**/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
**/ios/Podfile.lock
**/ios/Podfile
**/ios/Flutter/Flutter.podspec
lib/generated_plugin_registrant.dart
**/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
# Exceptions to above rules.
!**/ios/**/default.mode1v3
... ...
... ... @@ -180,13 +180,13 @@
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1010;
LastSwiftMigration = 1120;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
... ... @@ -386,7 +386,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
... ... @@ -520,7 +520,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
... ... @@ -546,7 +546,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.nfet.flutter.printingExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
... ...
... ... @@ -31,6 +31,30 @@ class MyAppState extends State<MyApp> {
final GlobalKey<State<StatefulWidget>> previewContainer = GlobalKey();
Printer selectedPrinter;
PrintingInfo printingInfo;
@override
void initState() {
Printing.info().then((PrintingInfo info) {
setState(() {
printingInfo = info;
});
});
super.initState();
}
void _showPrintedToast(bool printed) {
final ScaffoldState scaffold = Scaffold.of(shareWidget.currentContext);
if (printed) {
scaffold.showSnackBar(const SnackBar(
content: Text('Document printed successfully'),
));
} else {
scaffold.showSnackBar(const SnackBar(
content: Text('Document not printed'),
));
}
}
Future<void> _printPdf() async {
print('Print ...');
... ... @@ -38,7 +62,7 @@ class MyAppState extends State<MyApp> {
onLayout: (PdfPageFormat format) async =>
(await generateDocument(format)).save());
print('Document printed: $result');
_showPrintedToast(result);
}
Future<void> _saveAsFile() async {
... ... @@ -84,7 +108,7 @@ class MyAppState extends State<MyApp> {
onLayout: (PdfPageFormat format) async =>
(await generateDocument(PdfPageFormat.letter)).save());
print('Document printed: $result');
_showPrintedToast(result);
}
Future<void> _sharePdf() async {
... ... @@ -112,7 +136,8 @@ class MyAppState extends State<MyApp> {
await im.toByteData(format: ui.ImageByteFormat.rawRgba);
print('Print Screen ${im.width}x${im.height} ...');
Printing.layoutPdf(onLayout: (PdfPageFormat format) {
final bool result =
await Printing.layoutPdf(onLayout: (PdfPageFormat format) {
final pdf.Document document = pdf.Document();
final PdfImage image = PdfImage(document.document,
... ... @@ -132,24 +157,32 @@ class MyAppState extends State<MyApp> {
return document.save();
});
_showPrintedToast(result);
}
Future<void> _printHtml() async {
print('Print html ...');
await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
final bool result =
await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
final String html = await rootBundle.loadString('assets/example.html');
return await Printing.convertHtml(format: format, html: html);
});
_showPrintedToast(result);
}
Future<void> _printMarkdown() async {
print('Print Markdown ...');
await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
final bool result =
await Printing.layoutPdf(onLayout: (PdfPageFormat format) async {
final String md = await rootBundle.loadString('assets/example.md');
final String html = markdown.markdownToHtml(md,
extensionSet: markdown.ExtensionSet.gitHubWeb);
return await Printing.convertHtml(format: format, html: html);
});
_showPrintedToast(result);
}
@override
... ... @@ -170,37 +203,52 @@ class MyAppState extends State<MyApp> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
RaisedButton(
child: const Text('Print Document'), onPressed: _printPdf),
child: const Text('Print Document'),
onPressed: printingInfo?.canPrint ?? false ? _printPdf : null,
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
key: pickWidget,
child: const Text('Pick Printer'),
onPressed: _pickPrinter),
key: pickWidget,
child: const Text('Pick Printer'),
onPressed: printingInfo?.directPrint ?? false
? _pickPrinter
: null,
),
const SizedBox(width: 10),
RaisedButton(
child: Text(selectedPrinter == null
? 'Direct Print'
: 'Print to $selectedPrinter'),
onPressed:
selectedPrinter != null ? _directPrintPdf : null),
child: Text(selectedPrinter == null
? 'Direct Print'
: 'Print to $selectedPrinter'),
onPressed:
selectedPrinter != null ? _directPrintPdf : null,
),
],
),
RaisedButton(
key: shareWidget,
child: const Text('Share Document'),
onPressed: _sharePdf),
key: shareWidget,
child: const Text('Share Document'),
onPressed: printingInfo?.canShare ?? false ? _sharePdf : null,
),
RaisedButton(
child: const Text('Print Screenshot'),
onPressed: _printScreen),
child: const Text('Print Screenshot'),
onPressed:
printingInfo?.canPrint ?? false ? _printScreen : null,
),
RaisedButton(
child: const Text('Save to file'), onPressed: _saveAsFile),
RaisedButton(
child: const Text('Print Html'), onPressed: _printHtml),
child: const Text('Print Html'),
onPressed:
printingInfo?.canConvertHtml ?? false ? _printHtml : null,
),
RaisedButton(
child: const Text('Print Markdown'),
onPressed: _printMarkdown),
child: const Text('Print Markdown'),
onPressed: printingInfo?.canConvertHtml ?? false
? _printMarkdown
: null,
),
if (canDebug)
Row(
mainAxisSize: MainAxisSize.min,
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Flutter
func dataProviderReleaseDataCallback(info _: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size _: Int) {
data.deallocate()
}
class PdfPrintPageRenderer: UIPrintPageRenderer {
private var channel: FlutterMethodChannel?
private var pdfDocument: CGPDFDocument?
private var lock: NSLock?
private var mustLayout: Bool = true
init(_ channel: FlutterMethodChannel?, data: Data?) {
super.init()
self.channel = channel
pdfDocument = nil
if data != nil {
setDocument(data)
mustLayout = false
}
lock = NSLock()
}
override func drawPage(at pageIndex: Int, in _: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
let page = pdfDocument?.page(at: pageIndex + 1)
ctx?.scaleBy(x: 1.0, y: -1.0)
ctx?.translateBy(x: 0.0, y: -paperRect.size.height)
if page != nil {
ctx?.drawPDFPage(page!)
}
}
func cancelJob() {
pdfDocument = nil
lock?.unlock()
}
func setDocument(_ data: Data?) {
let bytesPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data?.count ?? 0)
data?.copyBytes(to: bytesPointer, count: data?.count ?? 0)
let dataProvider = CGDataProvider(dataInfo: nil, data: bytesPointer, size: data?.count ?? 0, releaseData: dataProviderReleaseDataCallback)
pdfDocument = CGPDFDocument(dataProvider!)
lock?.unlock()
}
override var numberOfPages: Int {
let width = NSNumber(value: Double(paperRect.size.width))
let height = NSNumber(value: Double(paperRect.size.height))
let marginLeft = NSNumber(value: Double(printableRect.origin.x))
let marginTop = NSNumber(value: Double(printableRect.origin.y))
let marginRight = NSNumber(value: Double(paperRect.size.width - (printableRect.origin.x + printableRect.size.width)))
let marginBottom = NSNumber(value: Double(paperRect.size.height - (printableRect.origin.y + printableRect.size.height)))
let arg = [
"width": width,
"height": height,
"marginLeft": marginLeft,
"marginTop": marginTop,
"marginRight": marginRight,
"marginBottom": marginBottom,
]
if mustLayout {
lock?.lock()
channel?.invokeMethod("onLayout", arguments: arg)
lock?.lock()
lock?.unlock()
}
let pages = pdfDocument?.numberOfPages ?? 0
return pages
}
var pageArgs: [String: NSNumber] {
let width = NSNumber(value: Double(paperRect.size.width))
let height = NSNumber(value: Double(paperRect.size.height))
let marginLeft = NSNumber(value: Double(printableRect.origin.x))
let marginTop = NSNumber(value: Double(printableRect.origin.y))
let marginRight = NSNumber(value: Double(paperRect.size.width - (printableRect.origin.x + printableRect.size.width)))
let marginBottom = NSNumber(value: Double(paperRect.size.height - (printableRect.origin.y + printableRect.size.height)))
return [
"width": width,
"height": height,
"marginLeft": marginLeft,
"marginTop": marginTop,
"marginRight": marginRight,
"marginBottom": marginBottom,
]
}
}
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Flutter
import WebKit
func dataProviderReleaseDataCallback(info _: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size _: Int) {
data.deallocate()
}
public class PrintJob: UIPrintPageRenderer, UIPrintInteractionControllerDelegate {
private var printing: PrintingPlugin
public var index: Int
private var pdfDocument: CGPDFDocument?
private var urlObservation: NSKeyValueObservation?
private var jobName: String?
public init(printing: PrintingPlugin, index: Int) {
self.printing = printing
self.index = index
pdfDocument = nil
super.init()
}
public override func drawPage(at pageIndex: Int, in _: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
let page = pdfDocument?.page(at: pageIndex + 1)
ctx?.scaleBy(x: 1.0, y: -1.0)
ctx?.translateBy(x: 0.0, y: -paperRect.size.height)
if page != nil {
ctx?.drawPDFPage(page!)
}
}
func cancelJob() {
pdfDocument = nil
}
func setDocument(_ data: Data?) {
let bytesPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data?.count ?? 0)
data?.copyBytes(to: bytesPointer, count: data?.count ?? 0)
let dataProvider = CGDataProvider(dataInfo: nil, data: bytesPointer, size: data?.count ?? 0, releaseData: dataProviderReleaseDataCallback)
pdfDocument = CGPDFDocument(dataProvider!)
let controller = UIPrintInteractionController.shared
controller.delegate = self
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = jobName!
printInfo.outputType = .general
controller.printInfo = printInfo
controller.printPageRenderer = self
controller.present(animated: true, completionHandler: completionHandler)
}
public override var numberOfPages: Int {
let pages = pdfDocument?.numberOfPages ?? 0
return pages
}
func completionHandler(printController _: UIPrintInteractionController, completed: Bool, error: Error?) {
if !completed, error != nil {
print("Unable to print: \(error?.localizedDescription ?? "unknown error")")
}
printing.onCompleted(printJob: self, completed: completed, error: error?.localizedDescription as NSString?)
}
func directPrintPdf(name: String, data: Data, withPrinter printerID: String) {
let printing = UIPrintInteractionController.isPrintingAvailable
if !printing {
self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
return
}
let controller = UIPrintInteractionController.shared
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = name
printInfo.outputType = .general
controller.printInfo = printInfo
controller.printingItem = data
let printerURL = URL(string: printerID)
if printerURL == nil {
self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
return
}
let printer = UIPrinter(url: printerURL!)
controller.print(to: printer, completionHandler: completionHandler)
}
func printPdf(name: String, withPageSize size: CGSize, andMargin margin: CGRect) {
let printing = UIPrintInteractionController.isPrintingAvailable
if !printing {
self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
return
}
jobName = name
self.printing.onLayout(
printJob: self,
width: size.width,
height: size.height,
marginLeft: margin.minX,
marginTop: margin.minY,
marginRight: size.width - margin.maxX,
marginBottom: size.height - margin.maxY
)
}
static func sharePdf(data: Data, withSourceRect rect: CGRect, andName name: String) {
let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let fileURL = tmpDirURL.appendingPathComponent(name)
do {
try data.write(to: fileURL, options: .atomic)
} catch {
print("sharePdf error: \(error.localizedDescription)")
return
}
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
if UI_USER_INTERFACE_IDIOM() == .pad {
let controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
activityViewController.popoverPresentationController?.sourceView = controller?.view
activityViewController.popoverPresentationController?.sourceRect = rect
}
UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)
}
func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {
let viewController = UIApplication.shared.delegate?.window?!.rootViewController
let wkWebView = WKWebView(frame: viewController!.view.bounds)
wkWebView.isHidden = true
wkWebView.tag = 100
viewController?.view.addSubview(wkWebView)
wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)
urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in
// this is workaround for issue with loading local images
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// assign the print formatter to the print page renderer
let renderer = UIPrintPageRenderer()
renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)
// assign paperRect and printableRect values
renderer.setValue(rect, forKey: "paperRect")
renderer.setValue(margin, forKey: "printableRect")
// create pdf context and draw each page
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, rect, nil)
for i in 0 ..< renderer.numberOfPages {
UIGraphicsBeginPDFPage()
renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
}
UIGraphicsEndPDFContext()
if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {
viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated
// clear WKWebView cache
if #available(iOS 9.0, *) {
WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
records.forEach { record in
WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
}
}
}
}
// dispose urlObservation
self.urlObservation = nil
self.printing.onHtmlRendered(printJob: self, pdfData: pdfData as Data)
}
})
}
static func pickPrinter(result: @escaping FlutterResult, withSourceRect rect: CGRect) {
let controller = UIPrinterPickerController(initiallySelectedPrinter: nil)
let pickPrinterCompletionHandler: UIPrinterPickerController.CompletionHandler = {
(printerPickerController: UIPrinterPickerController, completed: Bool, error: Error?) in
if !completed, error != nil {
print("Unable to pick printer: \(error?.localizedDescription ?? "unknown error")")
result(nil)
return
}
if printerPickerController.selectedPrinter == nil {
result(nil)
return
}
let printer = printerPickerController.selectedPrinter!
let data: NSDictionary = [
"url": printer.url.absoluteString as Any,
"name": printer.displayName as Any,
"model": printer.makeAndModel as Any,
"location": printer.displayLocation as Any,
]
result(data)
}
if UI_USER_INTERFACE_IDIOM() == .pad {
let viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
if viewController != nil {
controller.present(from: rect, in: viewController!.view, animated: true, completionHandler: pickPrinterCompletionHandler)
return
}
}
controller.present(animated: true, completionHandler: pickPrinterCompletionHandler)
}
public static func printingInfo() -> NSDictionary {
let data: NSDictionary = [
"directPrint": true,
"dynamicLayout": false,
"canPrint": true,
"canConvertHtml": true,
"canShare": true,
]
return data
}
}
... ...
... ... @@ -15,57 +15,66 @@
*/
import Flutter
import UIKit
import WebKit
import Foundation
public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControllerDelegate {
private var channel: FlutterMethodChannel?
private var renderer: PdfPrintPageRenderer?
private var urlObservation: NSKeyValueObservation?
public class PrintingPlugin: NSObject, FlutterPlugin {
private var channel: FlutterMethodChannel
init(_ channel: FlutterMethodChannel?) {
super.init()
init(_ channel: FlutterMethodChannel) {
self.channel = channel
renderer = nil
super.init()
}
/// Entry point
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "net.nfet.printing", binaryMessenger: registrar.messenger())
let instance = PrintingPlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
}
/// Flutter method handlers
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments! as! [String: Any]
if call.method == "printPdf" {
let name = args["name"] as? String ?? ""
let object = args["doc"] as? FlutterStandardTypedData
printPdf(name, data: object?.data)
let name = args["name"] as! String
let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0)
let height = CGFloat((args["height"] as? NSNumber)?.floatValue ?? 0.0)
let marginLeft = CGFloat((args["marginLeft"] as? NSNumber)?.floatValue ?? 0.0)
let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0)
let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0)
let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0)
let printJob = PrintJob(printing: self, index: args["job"] as! Int)
printJob.printPdf(name: name,
withPageSize: CGSize(
width: width,
height: height
),
andMargin: CGRect(
x: marginLeft,
y: marginTop,
width: width - marginRight - marginLeft,
height: height - marginBottom - marginTop
))
result(NSNumber(value: 1))
} else if call.method == "directPrintPdf" {
let name = args["name"] as? String ?? ""
let printer = args["printer"] as? String
let object = args["doc"] as? FlutterStandardTypedData
directPrintPdf(name: name, data: object!.data, withPrinter: printer!)
result(NSNumber(value: 1))
} else if call.method == "writePdf" {
if let object = args["doc"] as? FlutterStandardTypedData {
writePdf(object.data)
}
result(NSNumber(value: 1))
} else if call.method == "cancelJob" {
renderer?.cancelJob()
let controller = UIPrintInteractionController.shared
controller.dismiss(animated: true)
let name = args["name"] as! String
let printer = args["printer"] as! String
let object = args["doc"] as! FlutterStandardTypedData
let printJob = PrintJob(printing: self, index: args["job"] as! Int)
printJob.directPrintPdf(name: name, data: object.data, withPrinter: printer)
result(NSNumber(value: 1))
} else if call.method == "sharePdf" {
if let object = args["doc"] as? FlutterStandardTypedData {
sharePdf(
object,
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)),
andName: args["name"] as? String
)
}
let object = args["doc"] as! FlutterStandardTypedData
PrintJob.sharePdf(
data: object.data,
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)
),
andName: args["name"] as! String
)
result(NSNumber(value: 1))
} else if call.method == "convertHtml" {
let width = CGFloat((args["width"] as? NSNumber)?.floatValue ?? 0.0)
... ... @@ -74,8 +83,10 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll
let marginTop = CGFloat((args["marginTop"] as? NSNumber)?.floatValue ?? 0.0)
let marginRight = CGFloat((args["marginRight"] as? NSNumber)?.floatValue ?? 0.0)
let marginBottom = CGFloat((args["marginBottom"] as? NSNumber)?.floatValue ?? 0.0)
convertHtml(
(args["html"] as? String)!,
let printJob = PrintJob(printing: self, index: args["job"] as! Int)
printJob.convertHtml(
args["html"] as! String,
withPageSize: CGRect(
x: 0.0,
y: 0.0,
... ... @@ -88,219 +99,70 @@ public class PrintingPlugin: NSObject, FlutterPlugin, UIPrintInteractionControll
width: width - marginRight - marginLeft,
height: height - marginBottom - marginTop
),
andBaseUrl: args["baseUrl"] as? String == nil ? nil : URL(string: (args["baseUrl"] as? String)!)
andBaseUrl: args["baseUrl"] as? String == nil ? nil : URL(string: args["baseUrl"] as! String)
)
result(NSNumber(value: 1))
} else if call.method == "pickPrinter" {
pickPrinter(result, withSourceRect: CGRect(
PrintJob.pickPrinter(result: result, 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)
))
} else if call.method == "printingInfo" {
let data: NSDictionary = [
"iosVersion": UIDevice.current.systemVersion,
]
result(data)
result(PrintJob.printingInfo())
} else {
result(FlutterMethodNotImplemented)
}
}
func completionHandler(printController _: UIPrintInteractionController, completed: Bool, error: Error?) {
if !completed, error != nil {
print("Unable to print: \(error?.localizedDescription ?? "unknown error")")
}
/// Request the Pdf document from flutter
public func onLayout(printJob: PrintJob, width: CGFloat, height: CGFloat, marginLeft: CGFloat, marginTop: CGFloat, marginRight: CGFloat, marginBottom: CGFloat) {
let arg = [
"width": width,
"height": height,
"marginLeft": marginLeft,
"marginTop": marginTop,
"marginRight": marginRight,
"marginBottom": marginBottom,
"job": printJob.index,
] as [String: Any]
channel.invokeMethod("onLayout", arguments: arg, result: { (result: Any?) -> Void in
if result as? Bool == false {
printJob.cancelJob()
} else {
let object = result as! FlutterStandardTypedData
printJob.setDocument(object.data)
}
})
}
/// send completion status to flutter
public func onCompleted(printJob: PrintJob, completed: Bool, error: NSString?) {
let data: NSDictionary = [
"completed": completed,
"error": error?.localizedDescription as Any,
"error": error as Any,
"job": printJob.index,
]
channel?.invokeMethod("onCompleted", arguments: data)
renderer = nil
}
func directPrintPdf(name: String, data: Data, withPrinter printerID: String) {
let printing = UIPrintInteractionController.isPrintingAvailable
if !printing {
let data: NSDictionary = [
"completed": false,
"error": "Printing not available",
]
channel?.invokeMethod("onCompleted", arguments: data)
return
}
let controller = UIPrintInteractionController.shared
controller.delegate = self
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = name
printInfo.outputType = .general
controller.printInfo = printInfo
controller.printingItem = data
let printerURL = URL(string: printerID)
if printerURL == nil {
let data: NSDictionary = [
"completed": false,
"error": "Unable to fine printer URL",
]
channel?.invokeMethod("onCompleted", arguments: data)
return
}
let printer = UIPrinter(url: printerURL!)
controller.print(to: printer, completionHandler: completionHandler)
channel.invokeMethod("onCompleted", arguments: data)
}
func printPdf(_ name: String, data: Data?) {
let printing = UIPrintInteractionController.isPrintingAvailable
if !printing {
let data: NSDictionary = [
"completed": false,
"error": "Printing not available",
]
channel?.invokeMethod("onCompleted", arguments: data)
return
}
let controller = UIPrintInteractionController.shared
controller.delegate = self
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = name
printInfo.outputType = .general
controller.printInfo = printInfo
renderer = PdfPrintPageRenderer(channel, data: data)
controller.printPageRenderer = renderer
controller.present(animated: true, completionHandler: completionHandler)
}
func writePdf(_ data: Data) {
renderer?.setDocument(data)
}
func sharePdf(_ data: FlutterStandardTypedData, withSourceRect rect: CGRect, andName name: String?) {
let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let uuid = CFUUIDCreate(nil)
assert(uuid != nil)
let uuidStr = CFUUIDCreateString(nil, uuid)
assert(uuidStr != nil)
var fileURL: URL
if name == nil {
fileURL = tmpDirURL.appendingPathComponent("document-\(uuidStr ?? "1" as CFString)").appendingPathExtension("pdf")
} else {
fileURL = tmpDirURL.appendingPathComponent(name!)
}
do {
try data.data.write(to: fileURL, options: .atomic)
} catch {
print("sharePdf error: \(error.localizedDescription)")
return
}
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
if UI_USER_INTERFACE_IDIOM() == .pad {
let controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
activityViewController.popoverPresentationController?.sourceView = controller?.view
activityViewController.popoverPresentationController?.sourceRect = rect
}
UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)
}
func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {
let viewController = UIApplication.shared.delegate?.window?!.rootViewController
let wkWebView = WKWebView(frame: viewController!.view.bounds)
wkWebView.isHidden = true
wkWebView.tag = 100
viewController?.view.addSubview(wkWebView)
wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)
urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in
// this is workaround for issue with loading local images
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// assign the print formatter to the print page renderer
let renderer = UIPrintPageRenderer()
renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)
// assign paperRect and printableRect values
renderer.setValue(rect, forKey: "paperRect")
renderer.setValue(margin, forKey: "printableRect")
// create pdf context and draw each page
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, rect, nil)
for i in 0 ..< renderer.numberOfPages {
UIGraphicsBeginPDFPage()
renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
}
UIGraphicsEndPDFContext()
if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {
viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated
// clear WKWebView cache
if #available(iOS 9.0, *) {
WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
records.forEach { record in
WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
}
}
}
}
// dispose urlObservation
self.urlObservation = nil
let data = FlutterStandardTypedData(bytes: pdfData as Data)
self.channel?.invokeMethod("onHtmlRendered", arguments: data)
}
})
/// send html to pdf data result to flutter
public func onHtmlRendered(printJob: PrintJob, pdfData: Data) {
let data: NSDictionary = [
"doc": FlutterStandardTypedData(bytes: pdfData),
"job": printJob.index,
]
channel.invokeMethod("onHtmlRendered", arguments: data)
}
func pickPrinter(_ result: @escaping FlutterResult, withSourceRect rect: CGRect) {
let controller = UIPrinterPickerController(initiallySelectedPrinter: nil)
let pickPrinterCompletionHandler: UIPrinterPickerController.CompletionHandler = {
(printerPickerController: UIPrinterPickerController, completed: Bool, error: Error?) in
if !completed, error != nil {
print("Unable to pick printer: \(error?.localizedDescription ?? "unknown error")")
result(nil)
return
}
if printerPickerController.selectedPrinter == nil {
result(nil)
return
}
let printer = printerPickerController.selectedPrinter!
let data: NSDictionary = [
"url": printer.url.absoluteString as Any,
"name": printer.displayName as Any,
"model": printer.makeAndModel as Any,
"location": printer.displayLocation as Any,
]
result(data)
}
if UI_USER_INTERFACE_IDIOM() == .pad {
let viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
if viewController != nil {
controller.present(from: rect, in: viewController!.view, animated: true, completionHandler: pickPrinterCompletionHandler)
return
}
}
controller.present(animated: true, completionHandler: pickPrinterCompletionHandler)
/// send html to pdf conversion error to flutter
public func onHtmlError(printJob: PrintJob, error: String) {
let data: NSDictionary = [
"error": error,
"job": printJob.index,
]
channel.invokeMethod("onHtmlError", arguments: data)
}
}
... ...
... ... @@ -27,5 +27,8 @@ import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
part 'src/asset_utils.dart';
part 'src/print_job.dart';
part 'src/printer.dart';
part 'src/printing.dart';
part 'src/printing_info.dart';
part 'src/widgets.dart';
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
part of printing;
class _PrintJob {
_PrintJob({
this.onLayout,
this.onHtmlRendered,
this.onCompleted,
});
final LayoutCallback onLayout;
final Completer<List<int>> onHtmlRendered;
final Completer<bool> onCompleted;
int index;
}
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
part of printing;
@immutable
class Printer {
const Printer({
@required this.url,
this.name,
this.model,
this.location,
}) : assert(url != null);
final String url;
final String name;
final String model;
final String location;
@override
String toString() => name ?? url;
}
... ...
... ... @@ -18,74 +18,65 @@ part of printing;
typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format);
@immutable
class Printer {
const Printer({
@required this.url,
this.name,
this.model,
this.location,
}) : assert(url != null);
final String url;
final String name;
final String model;
final String location;
@override
String toString() => name ?? url;
}
mixin Printing {
static const MethodChannel _channel = MethodChannel('net.nfet.printing');
static LayoutCallback _onLayout;
static Completer<List<int>> _onHtmlRendered;
static Completer<bool> _onCompleted;
static final Map<int, _PrintJob> _printJobs = <int, _PrintJob>{};
static int _jobIndex = 0;
/// Callbacks from platform plugins
static Future<void> _handleMethod(MethodCall call) async {
static Future<dynamic> _handleMethod(MethodCall call) async {
switch (call.method) {
case 'onLayout':
final _PrintJob job = _printJobs[call.arguments['job']];
try {
final List<int> bytes = await _onLayout(PdfPageFormat(
final PdfPageFormat format = PdfPageFormat(
call.arguments['width'],
call.arguments['height'],
marginLeft: call.arguments['marginLeft'],
marginTop: call.arguments['marginTop'],
marginRight: call.arguments['marginRight'],
marginBottom: call.arguments['marginBottom'],
));
);
final List<int> bytes = await job.onLayout(format);
if (bytes == null) {
await _channel.invokeMethod<void>('cancelJob', <String, dynamic>{});
break;
return false;
}
final Map<String, dynamic> params = <String, dynamic>{
'doc': Uint8List.fromList(bytes),
};
await _channel.invokeMethod<void>('writePdf', params);
return Uint8List.fromList(bytes);
} catch (e) {
print('Unable to print: $e');
await _channel.invokeMethod<void>('cancelJob', <String, dynamic>{});
return false;
}
break;
case 'onCompleted':
final bool completed = call.arguments['completed'];
final String error = call.arguments['error'];
final _PrintJob job = _printJobs[call.arguments['job']];
if (completed == false && error != null) {
_onCompleted.completeError(error);
job.onCompleted.completeError(error);
} else {
_onCompleted.complete(completed);
job.onCompleted.complete(completed);
}
break;
case 'onHtmlRendered':
_onHtmlRendered.complete(call.arguments);
final _PrintJob job = _printJobs[call.arguments['job']];
job.onHtmlRendered.complete(call.arguments['doc']);
break;
case 'onHtmlError':
_onHtmlRendered.completeError(call.arguments);
final _PrintJob job = _printJobs[call.arguments['job']];
job.onHtmlRendered.completeError(call.arguments['error']);
break;
}
}
static _PrintJob _newPrintJob(_PrintJob job) {
job.index = _jobIndex++;
_printJobs[job.index] = job;
return job;
}
/// Prints a Pdf document to a local printer using the platform UI
/// the Pdf document is re-built in a [LayoutCallback] each time the
/// user changes a setting like the page format or orientation.
... ... @@ -98,24 +89,33 @@ mixin Printing {
String name = 'Document',
PdfPageFormat format = PdfPageFormat.standard,
}) async {
_onCompleted = Completer<bool>();
_onLayout = onLayout;
_channel.setMethodCallHandler(_handleMethod);
final Map<String, dynamic> params = <String, dynamic>{'name': name};
final _PrintJob job = _newPrintJob(_PrintJob(
onCompleted: Completer<bool>(),
onLayout: onLayout,
));
final Map<String, dynamic> params = <String, dynamic>{
'name': name,
'job': job.index,
'width': format.width,
'height': format.height,
'marginLeft': format.marginLeft,
'marginTop': format.marginTop,
'marginRight': format.marginRight,
'marginBottom': format.marginBottom,
};
await _channel.invokeMethod<int>('printPdf', params);
bool result = false;
try {
final Map<dynamic, dynamic> info = await printingInfo();
if (int.parse(info['iosVersion'].toString().split('.').first) >= 13) {
final List<int> bytes = await onLayout(format);
if (bytes == null) {
return false;
}
params['doc'] = Uint8List.fromList(bytes);
}
result = await job.onCompleted.future;
} catch (e) {
e.toString();
print('Document not printed: $e');
}
await _channel.invokeMethod<int>('printPdf', params);
return _onCompleted.future;
_printJobs.remove(job.index);
return result;
}
static Future<Map<dynamic, dynamic>> printingInfo() async {
... ... @@ -159,19 +159,31 @@ mixin Printing {
String name = 'Document',
PdfPageFormat format = PdfPageFormat.standard,
}) async {
_onCompleted = Completer<bool>();
if (printer == null) {
return false;
}
_channel.setMethodCallHandler(_handleMethod);
final _PrintJob job = _newPrintJob(_PrintJob(
onCompleted: Completer<bool>(),
));
final List<int> bytes = await onLayout(format);
if (bytes == null) {
return false;
}
final Map<String, dynamic> params = <String, dynamic>{
'name': name,
'printer': printer.url,
'doc': Uint8List.fromList(bytes),
'job': job.index,
};
await _channel.invokeMethod<int>('directPrintPdf', params);
return _onCompleted.future;
final bool result = await job.onCompleted.future;
_printJobs.remove(job.index);
return result;
}
/// Prints a [PdfDocument] or a pdf stream to a local printer using the platform UI
... ... @@ -192,11 +204,12 @@ mixin Printing {
static Future<void> sharePdf({
@Deprecated('use bytes with document.save()') PdfDocument document,
List<int> bytes,
String filename,
String filename = 'document.pdf',
Rect bounds,
}) async {
assert(document != null || bytes != null);
assert(!(document == null && bytes == null));
assert(filename != null);
if (document != null) {
bytes = document.save();
... ... @@ -220,6 +233,12 @@ mixin Printing {
{@required String html,
String baseUrl,
PdfPageFormat format = PdfPageFormat.a4}) async {
_channel.setMethodCallHandler(_handleMethod);
final _PrintJob job = _newPrintJob(_PrintJob(
onHtmlRendered: Completer<List<int>>(),
));
final Map<String, dynamic> params = <String, dynamic>{
'html': html,
'baseUrl': baseUrl,
... ... @@ -229,11 +248,29 @@ mixin Printing {
'marginTop': format.marginTop,
'marginRight': format.marginRight,
'marginBottom': format.marginBottom,
'job': job.index,
};
_channel.setMethodCallHandler(_handleMethod);
_onHtmlRendered = Completer<List<int>>();
await _channel.invokeMethod<void>('convertHtml', params);
return _onHtmlRendered.future;
final List<int> result = await job.onHtmlRendered.future;
_printJobs.remove(job.index);
return result;
}
static Future<PrintingInfo> info() async {
_channel.setMethodCallHandler(_handleMethod);
Map<dynamic, dynamic> result;
try {
result = await _channel.invokeMethod(
'printingInfo',
<String, dynamic>{},
);
} catch (e) {
print('Error getting printing info: $e');
return PrintingInfo.unavailable;
}
return PrintingInfo.fromMap(result);
}
}
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
part of printing;
class PrintingInfo {
factory PrintingInfo.fromMap(Map<dynamic, dynamic> map) => PrintingInfo._(
directPrint: map['directPrint'] ?? false,
dynamicLayout: map['dynamicLayout'] ?? false,
canPrint: map['canPrint'],
canConvertHtml: map['canConvertHtml'],
canShare: map['canShare'],
);
const PrintingInfo._({
this.directPrint = false,
this.dynamicLayout = false,
this.canPrint = false,
this.canConvertHtml = false,
this.canShare = false,
}) : assert(directPrint != null),
assert(dynamicLayout != null),
assert(canPrint != null),
assert(canConvertHtml != null),
assert(canShare != null);
static const PrintingInfo unavailable = PrintingInfo._();
final bool directPrint;
final bool dynamicLayout;
final bool canPrint;
final bool canConvertHtml;
final bool canShare;
}
... ...