David PHAM-VAN

Add Raster PDF to Image

# Changelog
## 3.0.2
- Add Raster PDF to Image
## 3.0.1
- Add a link to the Web example
... ...
... ... @@ -109,7 +109,7 @@ await Printing.layoutPdf(
```java
defaultConfig {
...
minSdkVersion 19 // <-- Change this line to 19 or more
minSdkVersion 21 // <-- Change this line to 21 or more
...
}
```
... ...
... ... @@ -18,11 +18,15 @@ package net.nfet.flutter.printing;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.pdf.PdfRenderer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PdfConvert;
... ... @@ -32,16 +36,20 @@ import android.print.PrintDocumentInfo;
import android.print.PrintJob;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.HashMap;
/**
... ... @@ -71,6 +79,7 @@ public class PrintingJob extends PrintDocumentAdapter {
result.put("canPrint", true);
result.put("canConvertHtml", true);
result.put("canShare", true);
result.put("canRaster", true);
return result;
}
... ... @@ -240,4 +249,79 @@ public class PrintingJob extends PrintDocumentAdapter {
// Content layout reflow is complete
callback.onLayoutFinished(info, true);
}
void rasterPdf(final byte[] data, final int[] pages, final Double scale) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
Log.e("PDF", "PDF Raster available since Android 5.0 Lollipop (API 21)");
printing.onPageRasterEnd(this);
return;
}
Thread thread = new Thread(new Runnable() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
try {
File file = File.createTempFile("printing", null, null);
FileOutputStream oStream = new FileOutputStream(file);
oStream.write(data);
oStream.close();
FileInputStream iStream = new FileInputStream(file);
ParcelFileDescriptor parcelFD = ParcelFileDescriptor.dup(iStream.getFD());
PdfRenderer renderer = new PdfRenderer(parcelFD);
if (!file.delete()) {
Log.e("PDF", "Unable to delete temporary file");
}
final int pageCount = pages != null ? pages.length : renderer.getPageCount();
for (int i = 0; i < pageCount; i++) {
PdfRenderer.Page page = renderer.openPage(pages == null ? i : pages[i]);
final int width = Double.valueOf(page.getWidth() * scale).intValue();
final int height = Double.valueOf(page.getHeight() * scale).intValue();
int stride = width * 4;
Matrix transform = new Matrix();
transform.setScale(scale.floatValue(), scale.floatValue());
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
page.render(
bitmap, null, transform, PdfRenderer.Page.RENDER_MODE_FOR_PRINT);
page.close();
final ByteBuffer buf = ByteBuffer.allocate(stride * height);
bitmap.copyPixelsToBuffer(buf);
bitmap.recycle();
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
printing.onPageRasterized(
PrintingJob.this, buf.array(), width, height);
}
});
}
renderer.close();
iStream.close();
} catch (IOException e) {
e.printStackTrace();
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
printing.onPageRasterEnd(PrintingJob.this);
}
});
}
});
thread.start();
}
}
... ...
... ... @@ -125,6 +125,16 @@ public class PrintingPlugin implements MethodCallHandler {
result.success(PrintingJob.printingInfo());
break;
}
case "rasterPdf": {
final byte[] document = call.argument("doc");
final int[] pages = call.argument("pages");
Double scale = call.argument("scale");
final PrintingJob printJob =
new PrintingJob(activity, this, (int) call.argument("job"));
printJob.rasterPdf(document, pages, scale);
result.success(1);
break;
}
default:
result.notImplemented();
break;
... ... @@ -194,4 +204,22 @@ public class PrintingPlugin implements MethodCallHandler {
channel.invokeMethod("onHtmlError", args);
}
/// send pdf to raster data result to flutter
void onPageRasterized(PrintingJob printJob, byte[] imageData, int width, int height) {
HashMap<String, Object> args = new HashMap<>();
args.put("image", imageData);
args.put("width", width);
args.put("height", height);
args.put("job", printJob.index);
channel.invokeMethod("onPageRasterized", args);
}
void onPageRasterEnd(PrintingJob printJob) {
HashMap<String, Object> args = new HashMap<>();
args.put("job", printJob.index);
channel.invokeMethod("onPageRasterEnd", args);
}
}
... ...
... ... @@ -34,7 +34,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
... ...
import 'package:flutter/material.dart';
class ImageViewer extends StatelessWidget {
const ImageViewer({this.images});
final List<ImageProvider> images;
Widget buildImage(BuildContext context, ImageProvider image) {
return Card(
margin: const EdgeInsets.all(10),
child: FittedBox(
child: Image(image: image),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Document as Images'),
),
body: ListView.builder(
itemCount: images.length,
itemBuilder: (BuildContext context, int index) =>
buildImage(context, images[index]),
),
);
}
}
... ...
... ... @@ -15,6 +15,7 @@ import 'package:pdf/widgets.dart' as pdf;
import 'package:printing/printing.dart';
import 'document.dart';
import 'image_viewer.dart';
import 'viewer.dart';
void main() {
... ... @@ -82,6 +83,22 @@ class MyAppState extends State<MyApp> {
);
}
Future<void> _rasterToImage() async {
final List<int> doc = (await generateDocument(PdfPageFormat.a4)).save();
final List<ImageProvider> images = <ImageProvider>[];
await for (PdfRaster page in Printing.raster(doc)) {
images.add(PdfRasterImage(page));
}
Navigator.push<dynamic>(
context,
MaterialPageRoute<dynamic>(
builder: (BuildContext context) => ImageViewer(images: images)),
);
}
Future<void> _pickPrinter() async {
print('Pick printer ...');
... ... @@ -243,6 +260,11 @@ class MyAppState extends State<MyApp> {
RaisedButton(
child: const Text('Save to file'), onPressed: _saveAsFile),
RaisedButton(
child: const Text('Raster to Image'),
onPressed:
printingInfo?.canRaster ?? false ? _rasterToImage : null,
),
RaisedButton(
child: const Text('Print Html'),
onPressed:
printingInfo?.canConvertHtml ?? false ? _printHtml : null,
... ...
... ... @@ -231,6 +231,41 @@ public class PrintJob: UIPrintPageRenderer, UIPrintInteractionControllerDelegate
controller.present(animated: true, completionHandler: pickPrinterCompletionHandler)
}
public func rasterPdf(data: Data, pages: [Int]?, scale: CGFloat) {
let provider = CGDataProvider(data: data as CFData)!
let document = CGPDFDocument(provider)!
DispatchQueue.global().async {
let pageCount = document.numberOfPages
for pageNum in pages ?? Array(0 ... pageCount - 1) {
guard let page = document.page(at: pageNum + 1) else { continue }
let rect = page.getBoxRect(.mediaBox)
let width = Int(rect.width * scale)
let height = Int(rect.height * scale)
let stride = width * 4
var data = Data(repeating: 0, count: stride * height)
data.withUnsafeMutableBytes { (ptr: UnsafeMutablePointer<UInt8>) in
let rgb = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: ptr, width: width, height: height, bitsPerComponent: 8, bytesPerRow: stride, space: rgb, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
if context != nil {
context!.scaleBy(x: scale, y: scale)
context!.drawPDFPage(page)
}
}
DispatchQueue.main.sync {
self.printing.onPageRasterized(printJob: self, imageData: data, width: width, height: height)
}
}
DispatchQueue.main.sync {
self.printing.onPageRasterEnd(printJob: self)
}
}
}
public static func printingInfo() -> NSDictionary {
let data: NSDictionary = [
"directPrint": true,
... ... @@ -238,6 +273,7 @@ public class PrintJob: UIPrintPageRenderer, UIPrintInteractionControllerDelegate
"canPrint": true,
"canConvertHtml": true,
"canShare": true,
"canRaster": true,
]
return data
}
... ...
... ... @@ -111,6 +111,15 @@ public class PrintingPlugin: NSObject, FlutterPlugin {
))
} else if call.method == "printingInfo" {
result(PrintJob.printingInfo())
} else if call.method == "rasterPdf" {
let doc = args["doc"] as! FlutterStandardTypedData
let pages = args["pages"] as? [Int]
let scale = CGFloat((args["scale"] as! NSNumber).floatValue)
let printJob = PrintJob(printing: self, index: args["job"] as! Int)
printJob.rasterPdf(data: doc.data,
pages: pages,
scale: scale)
result(NSNumber(value: 1))
} else {
result(FlutterMethodNotImplemented)
}
... ... @@ -165,4 +174,22 @@ public class PrintingPlugin: NSObject, FlutterPlugin {
]
channel.invokeMethod("onHtmlError", arguments: data)
}
/// send pdf to raster data result to flutter
public func onPageRasterized(printJob: PrintJob, imageData: Data, width: Int, height: Int) {
let data: NSDictionary = [
"image": FlutterStandardTypedData(bytes: imageData),
"width": width,
"height": height,
"job": printJob.index,
]
channel.invokeMethod("onPageRasterized", arguments: data)
}
public func onPageRasterEnd(printJob: PrintJob) {
let data: NSDictionary = [
"job": printJob.index,
]
channel.invokeMethod("onPageRasterEnd", arguments: data)
}
}
... ...
... ... @@ -31,4 +31,5 @@ part 'src/print_job.dart';
part 'src/printer.dart';
part 'src/printing.dart';
part 'src/printing_info.dart';
part 'src/raster.dart';
part 'src/widgets.dart';
... ...
... ... @@ -21,11 +21,13 @@ class _PrintJob {
this.onLayout,
this.onHtmlRendered,
this.onCompleted,
this.onPageRasterized,
});
final LayoutCallback onLayout;
final Completer<List<int>> onHtmlRendered;
final Completer<bool> onCompleted;
final StreamController<PdfRaster> onPageRasterized;
int index;
}
... ...
... ... @@ -68,6 +68,20 @@ mixin Printing {
final _PrintJob job = _printJobs[call.arguments['job']];
job.onHtmlRendered.completeError(call.arguments['error']);
break;
case 'onPageRasterized':
final _PrintJob job = _printJobs[call.arguments['job']];
final PdfRaster raster = PdfRaster._(
call.arguments['width'],
call.arguments['height'],
call.arguments['image'],
);
job.onPageRasterized.add(raster);
break;
case 'onPageRasterEnd':
final _PrintJob job = _printJobs[call.arguments['job']];
job.onPageRasterized.close();
_printJobs.remove(job.index);
break;
}
}
... ... @@ -273,4 +287,26 @@ mixin Printing {
return PrintingInfo.fromMap(result);
}
static Stream<PdfRaster> raster(
List<int> document, {
List<int> pages,
double dpi = PdfPageFormat.inch,
}) {
_channel.setMethodCallHandler(_handleMethod);
final _PrintJob job = _newPrintJob(_PrintJob(
onPageRasterized: StreamController<PdfRaster>(),
));
final Map<String, dynamic> params = <String, dynamic>{
'doc': Uint8List.fromList(document),
'pages': pages,
'scale': dpi / PdfPageFormat.inch,
'job': job.index,
};
_channel.invokeMethod<void>('rasterPdf', params);
return job.onPageRasterized.stream;
}
}
... ...
... ... @@ -23,6 +23,7 @@ class PrintingInfo {
canPrint: map['canPrint'],
canConvertHtml: map['canConvertHtml'],
canShare: map['canShare'],
canRaster: map['canRaster'],
);
const PrintingInfo._({
... ... @@ -31,11 +32,13 @@ class PrintingInfo {
this.canPrint = false,
this.canConvertHtml = false,
this.canShare = false,
this.canRaster = false,
}) : assert(directPrint != null),
assert(dynamicLayout != null),
assert(canPrint != null),
assert(canConvertHtml != null),
assert(canShare != null);
assert(canShare != null),
assert(canRaster != null);
static const PrintingInfo unavailable = PrintingInfo._();
... ... @@ -44,4 +47,5 @@ class PrintingInfo {
final bool canPrint;
final bool canConvertHtml;
final bool canShare;
final bool canRaster;
}
... ...
/*
* 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 PdfRaster {
PdfRaster._(
this.width,
this.height,
this.pixels,
);
final int width;
final int height;
final Uint8List pixels;
@override
String toString() => 'Image ${width}x$height ${pixels.lengthInBytes} bytes';
/// Decode RGBA raw image to dart:ui Image
Future<ui.Image> toImage() {
final Completer<ui.Image> comp = Completer<ui.Image>();
ui.decodeImageFromPixels(
pixels,
width,
height,
ui.PixelFormat.rgba8888,
(ui.Image image) => comp.complete(image),
);
return comp.future;
}
/// Convert to a PNG image
Future<Uint8List> toPng() async {
final ui.Image image = await toImage();
final ByteData data =
await image.toByteData(format: ui.ImageByteFormat.png);
return data.buffer.asUint8List();
}
}
class PdfRasterImage extends ImageProvider<PdfRaster> {
PdfRasterImage(this.raster);
final PdfRaster raster;
Future<ImageInfo> _loadAsync() async {
final ui.Image uiImage = await raster.toImage();
return ImageInfo(image: uiImage, scale: 1);
}
@override
ImageStreamCompleter load(PdfRaster key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(_loadAsync());
}
@override
Future<PdfRaster> obtainKey(ImageConfiguration configuration) async {
return raster;
}
}
... ...
... ... @@ -168,6 +168,41 @@ public class PrintJob: NSView, NSSharingServicePickerDelegate {
result(NSNumber(value: 1))
}
public func rasterPdf(data: Data, pages: [Int]?, scale: CGFloat) {
let provider = CGDataProvider(data: data as CFData)!
let document = CGPDFDocument(provider)!
DispatchQueue.global().async {
let pageCount = document.numberOfPages
for pageNum in pages ?? Array(0 ... pageCount - 1) {
guard let page = document.page(at: pageNum + 1) else { continue }
let rect = page.getBoxRect(.mediaBox)
let width = Int(rect.width * scale)
let height = Int(rect.height * scale)
let stride = width * 4
var data = Data(repeating: 0, count: stride * height)
data.withUnsafeMutableBytes { (ptr: UnsafeMutablePointer<UInt8>) in
let rgb = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: ptr, width: width, height: height, bitsPerComponent: 8, bytesPerRow: stride, space: rgb, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
if context != nil {
context!.scaleBy(x: scale, y: scale)
context!.drawPDFPage(page)
}
}
DispatchQueue.main.sync {
self.printing.onPageRasterized(printJob: self, imageData: data, width: width, height: height)
}
}
DispatchQueue.main.sync {
self.printing.onPageRasterEnd(printJob: self)
}
}
}
public static func printingInfo() -> NSDictionary {
let data: NSDictionary = [
"directPrint": false,
... ... @@ -175,6 +210,7 @@ public class PrintJob: NSView, NSSharingServicePickerDelegate {
"canPrint": true,
"canConvertHtml": true,
"canShare": true,
"canRaster": true,
]
return data
}
... ...
... ... @@ -111,6 +111,15 @@ public class PrintingPlugin: NSObject, FlutterPlugin {
// ))
} else if call.method == "printingInfo" {
result(PrintJob.printingInfo())
} else if call.method == "rasterPdf" {
let doc = args["doc"] as! FlutterStandardTypedData
let pages = args["pages"] as? [Int]
let scale = CGFloat((args["scale"] as! NSNumber).floatValue)
let printJob = PrintJob(printing: self, index: args["job"] as! Int)
printJob.rasterPdf(data: doc.data,
pages: pages,
scale: scale)
result(NSNumber(value: 1))
} else {
result(FlutterMethodNotImplemented)
}
... ... @@ -165,4 +174,22 @@ public class PrintingPlugin: NSObject, FlutterPlugin {
]
channel.invokeMethod("onHtmlError", arguments: data)
}
/// send pdf to raster data result to flutter
public func onPageRasterized(printJob: PrintJob, imageData: Data, width: Int, height: Int) {
let data: NSDictionary = [
"image": FlutterStandardTypedData(bytes: imageData),
"width": width,
"height": height,
"job": printJob.index,
]
channel.invokeMethod("onPageRasterized", arguments: data)
}
public func onPageRasterEnd(printJob: PrintJob) {
let data: NSDictionary = [
"job": printJob.index,
]
channel.invokeMethod("onPageRasterEnd", arguments: data)
}
}
... ...
... ... @@ -4,7 +4,7 @@ description: Plugin that allows Flutter apps to generate and print documents to
homepage: https://github.com/DavBfr/dart_pdf/tree/master/printing
repository: https://github.com/DavBfr/dart_pdf
issue_tracker: https://github.com/DavBfr/dart_pdf/issues
version: 3.0.1
version: 3.0.2
environment:
sdk: ">=2.3.0 <3.0.0"
... ...