PrintJob.swift
16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
/*
* 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()
}
// A variable that holds the selected printers to prevent recreate it if selected again
// Each printer will be identified by its URL string
var selectedPrinters = [String: UIPrinter]()
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?
private var printerName: String?
private var orientation: UIPrintInfo.Orientation?
private let semaphore = DispatchSemaphore(value: 0)
private var dynamic = false
private var currentSize: CGSize?
public init(printing: PrintingPlugin, index: Int) {
self.printing = printing
self.index = index
pdfDocument = nil
super.init()
}
override public 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(_ error: String?) {
pdfDocument = nil
if dynamic {
semaphore.signal()
} else {
printing.onCompleted(printJob: self, completed: false, error: error as NSString?)
}
}
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!)
if dynamic {
// Unblock the main thread
semaphore.signal()
return
}
DispatchQueue.main.async { [self] in
let controller = UIPrintInteractionController.shared
controller.delegate = self
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = jobName!
printInfo.outputType = .general
if orientation != nil {
printInfo.orientation = orientation!
orientation = nil
}
controller.printInfo = printInfo
controller.printPageRenderer = self
if self.printerName != nil {
let printerURL = URL(string: self.printerName!)
if printerURL == nil {
self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
return
}
let printerURLString = printerURL!.absoluteString
if !selectedPrinters.keys.contains(printerURLString) {
selectedPrinters[printerURLString] = UIPrinter(url: printerURL!)
}
selectedPrinters[printerURLString]!.contactPrinter { available in
if !available {
self.printing.onCompleted(printJob: self, completed: false, error: "Printer not available")
return
}
controller.print(to: selectedPrinters[printerURLString]!, completionHandler: self.completionHandler)
}
} else {
controller.present(animated: true, completionHandler: self.completionHandler)
}
}
}
override public var numberOfPages: Int {
if dynamic {
printing.onLayout(
printJob: self,
width: paperRect.size.width,
height: paperRect.size.height,
marginLeft: printableRect.origin.x,
marginTop: printableRect.origin.y,
marginRight: paperRect.size.width - (printableRect.origin.x + printableRect.size.width),
marginBottom: paperRect.size.height - (printableRect.origin.y + printableRect.size.height)
)
// Block the main thread, waiting for a document
semaphore.wait()
}
return pdfDocument?.numberOfPages ?? 0
}
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?)
}
public func printInteractionController(_: UIPrintInteractionController, choosePaper paperList: [UIPrintPaper]) -> UIPrintPaper {
if currentSize == nil {
return paperList[0]
}
for paper in paperList {
if (paper.paperSize.width == currentSize!.width && paper.paperSize.height == currentSize!.height) ||
(paper.paperSize.width == currentSize!.height && paper.paperSize.height == currentSize!.width)
{
return paper
}
}
let bestPaper = UIPrintPaper.bestPaper(forPageSize: currentSize!, withPapersFrom: paperList)
return bestPaper
}
func printPdf(name: String, withPageSize size: CGSize, andMargin margin: CGRect, withPrinter printerID: String?, dynamically dyn: Bool, outputType type: UIPrintInfo.OutputType) {
currentSize = size
dynamic = dyn
let printing = UIPrintInteractionController.isPrintingAvailable
if !printing {
self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
return
}
if size.width > size.height {
orientation = UIPrintInfo.Orientation.landscape
}
jobName = name
printerName = printerID
let controller = UIPrintInteractionController.shared
controller.delegate = self
let printInfo = UIPrintInfo.printInfo()
printInfo.jobName = jobName!
printInfo.outputType = type
if orientation != nil {
printInfo.orientation = orientation!
orientation = nil
}
controller.printInfo = printInfo
controller.showsPaperSelectionForLoadedPapers = true
controller.printPageRenderer = self
if printerID != nil {
let printerURL = URL(string: printerID!)
if printerURL == nil {
self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
return
}
let printerURLString = printerURL!.absoluteString
if !selectedPrinters.keys.contains(printerURLString) {
selectedPrinters[printerURLString] = UIPrinter(url: printerURL!)
}
selectedPrinters[printerURLString]!.contactPrinter { available in
if !available {
self.printing.onCompleted(printJob: self, completed: false, error: "Printer not available")
return
}
controller.print(to: selectedPrinters[printerURLString]!, completionHandler: self.completionHandler)
}
return
}
if dynamic {
controller.present(animated: true, completionHandler: completionHandler)
return
}
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, subject: String?, body: 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, body as Any], applicationActivities: nil)
activityViewController.setValue(subject, forKey: "subject")
if UIDevice.current.userInterfaceIdiom == .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
for record in records {
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 UIDevice.current.userInterfaceIdiom == .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 func rasterPdf(data: Data, pages: [Int]?, scale: CGFloat) {
guard
let provider = CGDataProvider(data: data as CFData),
let document = CGPDFDocument(provider)
else {
printing.onPageRasterEnd(printJob: self, error: "Cannot raster a malformed PDF file")
return
}
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 angle = CGFloat(page.rotationAngle) * CGFloat.pi / -180
let rect = page.getBoxRect(.mediaBox)
let rectCrop = page.getBoxRect(.cropBox)
let diffHeight = rectCrop.height - rect.height
let diffWidth = rectCrop.width - rect.width
let width = Int(abs((cos(angle) * rectCrop.width + sin(angle) * rectCrop.height) * scale))
let height = Int(abs((cos(angle) * rectCrop.height + sin(angle) * rectCrop.width) * scale))
let stride = width * 4
var data = Data(repeating: 0, count: stride * height)
data.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) in
let rgb = CGColorSpaceCreateDeviceRGB()
let context = CGContext(
data: outputBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: stride,
space: rgb,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
if context != nil {
context!.translateBy(x: CGFloat(width) / 2, y: CGFloat(height) / 2)
context!.scaleBy(x: scale, y: scale)
context!.rotate(by: angle)
context!.translateBy(x: -rectCrop.width / 2, y: -rectCrop.height / 2)
context!.translateBy(x: diffWidth, y: diffHeight)
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, error: nil)
}
}
}
public static func printingInfo() -> NSDictionary {
let data: NSDictionary = [
"directPrint": true,
"dynamicLayout": true,
"canPrint": true,
"canShare": true,
"canRaster": true,
"canListPrinters": false,
]
return data
}
}