David PHAM-VAN

Implement asynchronous printing

# 1.2.0
* Fix compileSdkVersion to match appcompat
* Change license to Apache 2.0
* Implement asynchronous printing driven by the OS
# 1.1.0
* Rename classes to satisfy Dart conventions
... ...
... ... @@ -16,6 +16,8 @@
package net.nfet.flutter.printing;
import static java.lang.StrictMath.max;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
... ... @@ -36,6 +38,7 @@ 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;
import io.flutter.plugin.common.MethodChannel;
... ... @@ -46,12 +49,18 @@ import io.flutter.plugin.common.PluginRegistry.Registrar;
/**
* PrintingPlugin
*/
public class PrintingPlugin implements MethodCallHandler {
public class PrintingPlugin extends PrintDocumentAdapter implements MethodCallHandler {
private static PrintManager printManager;
private final Activity activity;
private final MethodChannel channel;
private PrintedPdfDocument mPdfDocument;
private byte[] documentData;
private String jobName;
private LayoutResultCallback callback;
private PrintingPlugin(Activity activity) {
private PrintingPlugin(Activity activity, MethodChannel channel) {
this.activity = activity;
this.channel = channel;
}
/**
... ... @@ -59,82 +68,97 @@ public class PrintingPlugin implements MethodCallHandler {
*/
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "printing");
channel.setMethodCallHandler(new PrintingPlugin(registrar.activity()));
channel.setMethodCallHandler(new PrintingPlugin(registrar.activity(), channel));
printManager = (PrintManager) registrar.activity().getSystemService(Context.PRINT_SERVICE);
}
@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() {
// noinspection ResultOfMethodCallIgnored
}
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
case "printPdf":
printPdf((byte[]) call.argument("doc"));
jobName =
call.argument("name") == null ? "Document" : (String) call.argument("name");
assert jobName != null;
printManager.print(jobName, this, null);
result.success(0);
break;
case "sharePdf":
sharePdf((byte[]) call.argument("doc"));
result.success(0);
break;
default:
result.notImplemented();
break;
}
}
private void printPdf(final byte[] documentData) {
PrintDocumentAdapter pda = new PrintDocumentAdapter() {
PrintedPdfDocument mPdfDocument;
@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;
}
case "writePdf":
documentData = (byte[]) call.argument("doc");
// Return print information to print framework
PrintDocumentInfo info =
new PrintDocumentInfo.Builder("document.pdf")
new PrintDocumentInfo.Builder(jobName + ".pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.build();
// Content layout reflow is complete
callback.onLayoutFinished(info, true);
}
@Override
public void onFinish() {
// noinspection ResultOfMethodCallIgnored
}
};
String jobName = "Document";
printManager.print(jobName, pda, null);
result.success(0);
break;
case "sharePdf":
sharePdf((byte[]) call.argument("doc"));
result.success(0);
break;
default:
result.notImplemented();
break;
}
}
private void sharePdf(byte[] data) {
... ...
... ... @@ -25,8 +25,9 @@ class MyAppState extends State<MyApp> {
void _printPdf() async {
print("Print ...");
final pdf = await generateDocument(PdfPageFormat.a4);
Printing.printPdf(document: pdf);
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async =>
(await generateDocument(format)).save());
}
void _sharePdf() async {
... ... @@ -46,45 +47,49 @@ class MyAppState extends State<MyApp> {
}
Future<void> _printScreen() async {
final pdf = PdfDocument(deflate: zlib.encode);
final page = PdfPage(pdf, pageFormat: PdfPageFormat.a4);
final g = page.getGraphics();
RenderRepaintBoundary boundary =
previewContainer.currentContext.findRenderObject();
final im = await boundary.toImage();
final bytes = await im.toByteData(format: ImageByteFormat.rawRgba);
print("Print Screen ${im.width}x${im.height} ...");
// Center the image
final w = page.pageFormat.width -
page.pageFormat.marginLeft -
page.pageFormat.marginRight;
final h = page.pageFormat.height -
page.pageFormat.marginTop -
page.pageFormat.marginBottom;
double iw, ih;
if (im.width.toDouble() / im.height.toDouble() < 1.0) {
ih = h;
iw = im.width.toDouble() * ih / im.height.toDouble();
} else {
iw = w;
ih = im.height.toDouble() * iw / im.width.toDouble();
}
PdfImage image = PdfImage(pdf,
image: bytes.buffer.asUint8List(), width: im.width, height: im.height);
g.drawImage(
image,
page.pageFormat.marginLeft + (w - iw) / 2.0,
page.pageFormat.height -
page.pageFormat.marginTop -
ih -
(h - ih) / 2.0,
iw,
ih);
Printing.printPdf(document: pdf);
Printing.layoutPdf(onLayout: (PdfPageFormat format) {
final pdf = PdfDocument(deflate: zlib.encode);
final page = PdfPage(pdf, pageFormat: format);
final g = page.getGraphics();
// Center the image
final w = page.pageFormat.width -
page.pageFormat.marginLeft -
page.pageFormat.marginRight;
final h = page.pageFormat.height -
page.pageFormat.marginTop -
page.pageFormat.marginBottom;
double iw, ih;
if (im.width.toDouble() / im.height.toDouble() < 1.0) {
ih = h;
iw = im.width.toDouble() * ih / im.height.toDouble();
} else {
iw = w;
ih = im.height.toDouble() * iw / im.width.toDouble();
}
PdfImage image = PdfImage(pdf,
image: bytes.buffer.asUint8List(),
width: im.width,
height: im.height);
g.drawImage(
image,
page.pageFormat.marginLeft + (w - iw) / 2.0,
page.pageFormat.height -
page.pageFormat.marginTop -
ih -
(h - ih) / 2.0,
iw,
ih);
return pdf.save();
});
}
@override
... ...
/*
* 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/Flutter.h>
@interface PdfPrintPageRenderer : UIPrintPageRenderer
- (instancetype)init:(FlutterMethodChannel*)channel;
- (void)drawPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)printableRect;
@property(nonatomic, readonly) NSInteger numberOfPages;
@property(nonatomic, readonly) NSLock* lock;
@property(nonatomic) CGPDFDocumentRef pdfDocument;
@end
... ...
/*
* 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 "PageRenderer.h"
@implementation PdfPrintPageRenderer {
FlutterMethodChannel* channel;
}
@synthesize lock;
@synthesize pdfDocument;
- (instancetype)init:(FlutterMethodChannel*)channel {
self = [super init];
self->channel = channel;
self->lock = [[NSLock alloc] init];
self->pdfDocument = nil;
return self;
}
- (NSInteger)numberOfPages {
NSNumber* width = [[NSNumber alloc] initWithDouble:self.paperRect.size.width];
NSNumber* height =
[[NSNumber alloc] initWithDouble:self.paperRect.size.height];
NSNumber* marginLeft =
[[NSNumber alloc] initWithDouble:self.printableRect.origin.x];
NSNumber* marginTop =
[[NSNumber alloc] initWithDouble:self.printableRect.origin.y];
NSNumber* marginRight =
[[NSNumber alloc] initWithDouble:self.paperRect.size.width -
(self.printableRect.origin.x +
self.printableRect.size.width)];
NSNumber* marginBottom =
[[NSNumber alloc] initWithDouble:self.paperRect.size.height -
(self.printableRect.origin.y +
self.printableRect.size.height)];
NSDictionary* arg = @{
@"width" : width,
@"height" : height,
@"marginLeft" : marginLeft,
@"marginTop" : marginTop,
@"marginRight" : marginRight,
@"marginBottom" : marginBottom,
};
[lock lock];
[channel invokeMethod:@"onLayout" arguments:arg];
[lock lock];
[lock unlock];
size_t pages = CGPDFDocumentGetNumberOfPages(pdfDocument);
return pages;
}
- (void)drawPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)printableRect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, pageIndex + 1);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextTranslateCTM(ctx, 0.0, -self.paperRect.size.height);
CGContextDrawPDFPage(ctx, page);
}
@end
... ...
... ... @@ -18,4 +18,6 @@
@interface PrintingPlugin
: NSObject <FlutterPlugin, UIPrintInteractionControllerDelegate>
- (instancetype)init:(FlutterMethodChannel*)channel;
@end
... ...
... ... @@ -15,19 +15,34 @@
*/
#import "PrintingPlugin.h"
#import "PageRenderer.h"
@implementation PrintingPlugin {
FlutterMethodChannel* channel;
PdfPrintPageRenderer* renderer;
}
@implementation PrintingPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel =
[FlutterMethodChannel methodChannelWithName:@"printing"
binaryMessenger:[registrar messenger]];
PrintingPlugin* instance = [[PrintingPlugin alloc] init];
PrintingPlugin* instance = [[PrintingPlugin alloc] init:channel];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (instancetype)init:(FlutterMethodChannel*)channel {
self = [super init];
self->channel = channel;
self->renderer = [[PdfPrintPageRenderer alloc] init:channel];
return self;
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"printPdf" isEqualToString:call.method]) {
[self printPdf:[call.arguments objectForKey:@"doc"]];
[self printPdf:[call.arguments objectForKey:@"name"]];
result(@1);
} else if ([@"writePdf" isEqualToString:call.method]) {
[self writePdf:[call.arguments objectForKey:@"doc"]];
result(@1);
} else if ([@"sharePdf" isEqualToString:call.method]) {
[self sharePdf:[call.arguments objectForKey:@"doc"]
... ... @@ -42,22 +57,22 @@
}
}
- (void)printPdf:(nonnull FlutterStandardTypedData*)data {
- (void)printPdf:(nonnull NSString*)name {
BOOL printing = [UIPrintInteractionController isPrintingAvailable];
if (!printing) {
NSLog(@"printing not available");
return;
}
BOOL dataOK = [UIPrintInteractionController canPrintData:[data data]];
if (!dataOK)
NSLog(@"data not ok to be printed");
UIPrintInteractionController* controller =
[UIPrintInteractionController sharedPrintController];
[controller setDelegate:self];
[controller setPrintingItem:[data data]];
UIPrintInfo* printInfo = [UIPrintInfo printInfo];
printInfo.jobName = name;
printInfo.outputType = UIPrintInfoOutputGeneral;
controller.printInfo = printInfo;
[controller setPrintPageRenderer:renderer];
UIPrintInteractionCompletionHandler completionHandler =
^(UIPrintInteractionController* printController, BOOL completed,
NSError* error) {
... ... @@ -65,11 +80,27 @@
NSLog(@"FAILED! due to error in domain %@ with error code %u",
error.domain, (unsigned int)error.code);
}
if (self->renderer.pdfDocument != nil) {
CGPDFDocumentRelease(self->renderer.pdfDocument);
self->renderer.pdfDocument = nil;
}
};
[controller presentAnimated:YES completionHandler:completionHandler];
}
- (void)writePdf:(nonnull FlutterStandardTypedData*)data {
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(
NULL, data.data.bytes, data.data.length, NULL);
if (renderer.pdfDocument != nil) {
CGPDFDocumentRelease(renderer.pdfDocument);
renderer.pdfDocument = nil;
}
renderer.pdfDocument = CGPDFDocumentCreateWithProvider(dataProvider);
CGDataProviderRelease(dataProvider);
[renderer.lock unlock];
}
- (void)sharePdf:(nonnull FlutterStandardTypedData*)data
withSourceRect:(CGRect)rect {
NSURL* tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()
... ...
... ... @@ -16,20 +16,47 @@
part of printing;
typedef LayoutCallback = FutureOr<List<int>> Function(PdfPageFormat format);
class Printing {
static const MethodChannel _channel = MethodChannel('printing');
static LayoutCallback _onLayout;
static Future<dynamic> _handleMethod(MethodCall call) async {
switch (call.method) {
case "onLayout":
final bytes = await _onLayout(PdfPageFormat(
call.arguments['width'],
call.arguments['height'],
marginLeft: call.arguments['marginLeft'],
marginTop: call.arguments['marginTop'],
marginRight: call.arguments['marginRight'],
marginBottom: call.arguments['marginBottom'],
));
final Map<String, dynamic> params = <String, dynamic>{
'doc': Uint8List.fromList(bytes),
};
await _channel.invokeMethod('writePdf', params);
return Future.value("");
}
}
static Future<Null> layoutPdf(
{@required LayoutCallback onLayout, String name = "Document"}) async {
_onLayout = onLayout;
_channel.setMethodCallHandler(_handleMethod);
final Map<String, dynamic> params = <String, dynamic>{'name': name};
await _channel.invokeMethod('printPdf', params);
}
@deprecated
static Future<Null> printPdf({PdfDocument document, List<int> bytes}) async {
assert(document != null || bytes != null);
assert(!(document == null && bytes == null));
if (document != null) bytes = document.save();
final Map<String, dynamic> params = <String, dynamic>{
'doc': Uint8List.fromList(bytes),
};
await _channel.invokeMethod('printPdf', params);
layoutPdf(
onLayout: (PdfPageFormat format) =>
document != null ? document.save() : bytes);
}
static Future<Null> sharePdf(
... ...