PrintJob.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /*
  2. * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Flutter
  17. import WebKit
  18. func dataProviderReleaseDataCallback(info _: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size _: Int) {
  19. data.deallocate()
  20. }
  21. public class PrintJob: UIPrintPageRenderer, UIPrintInteractionControllerDelegate {
  22. private var printing: PrintingPlugin
  23. public var index: Int
  24. private var pdfDocument: CGPDFDocument?
  25. private var urlObservation: NSKeyValueObservation?
  26. private var jobName: String?
  27. private var printerName: String?
  28. private var orientation: UIPrintInfo.Orientation?
  29. private let semaphore = DispatchSemaphore(value: 0)
  30. private var dynamic = false
  31. private var currentSize: CGSize?
  32. public init(printing: PrintingPlugin, index: Int) {
  33. self.printing = printing
  34. self.index = index
  35. pdfDocument = nil
  36. super.init()
  37. }
  38. override public func drawPage(at pageIndex: Int, in _: CGRect) {
  39. let ctx = UIGraphicsGetCurrentContext()
  40. let page = pdfDocument?.page(at: pageIndex + 1)
  41. ctx?.scaleBy(x: 1.0, y: -1.0)
  42. ctx?.translateBy(x: 0.0, y: -paperRect.size.height)
  43. if page != nil {
  44. ctx?.drawPDFPage(page!)
  45. }
  46. }
  47. func cancelJob(_ error: String?) {
  48. pdfDocument = nil
  49. if dynamic {
  50. semaphore.signal()
  51. } else {
  52. printing.onCompleted(printJob: self, completed: false, error: error as NSString?)
  53. }
  54. }
  55. func setDocument(_ data: Data?) {
  56. let bytesPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data?.count ?? 0)
  57. data?.copyBytes(to: bytesPointer, count: data?.count ?? 0)
  58. let dataProvider = CGDataProvider(dataInfo: nil, data: bytesPointer, size: data?.count ?? 0, releaseData: dataProviderReleaseDataCallback)
  59. pdfDocument = CGPDFDocument(dataProvider!)
  60. if dynamic {
  61. // Unblock the main thread
  62. semaphore.signal()
  63. return
  64. }
  65. DispatchQueue.main.async { [self] in
  66. let controller = UIPrintInteractionController.shared
  67. controller.delegate = self
  68. let printInfo = UIPrintInfo.printInfo()
  69. printInfo.jobName = jobName!
  70. printInfo.outputType = .general
  71. if orientation != nil {
  72. printInfo.orientation = orientation!
  73. orientation = nil
  74. }
  75. controller.printInfo = printInfo
  76. controller.printPageRenderer = self
  77. if self.printerName != nil {
  78. let printerURL = URL(string: self.printerName!)
  79. if printerURL == nil {
  80. self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
  81. return
  82. }
  83. let printer = UIPrinter(url: printerURL!)
  84. printer.contactPrinter { available in
  85. if !available {
  86. self.printing.onCompleted(printJob: self, completed: false, error: "Printer not available")
  87. return
  88. }
  89. controller.print(to: printer, completionHandler: self.completionHandler)
  90. }
  91. } else {
  92. controller.present(animated: true, completionHandler: self.completionHandler)
  93. }
  94. }
  95. }
  96. override public var numberOfPages: Int {
  97. if dynamic {
  98. printing.onLayout(
  99. printJob: self,
  100. width: paperRect.size.width,
  101. height: paperRect.size.height,
  102. marginLeft: printableRect.origin.x,
  103. marginTop: printableRect.origin.y,
  104. marginRight: paperRect.size.width - (printableRect.origin.x + printableRect.size.width),
  105. marginBottom: paperRect.size.height - (printableRect.origin.y + printableRect.size.height)
  106. )
  107. // Block the main thread, waiting for a document
  108. semaphore.wait()
  109. }
  110. return pdfDocument?.numberOfPages ?? 0
  111. }
  112. func completionHandler(printController _: UIPrintInteractionController, completed: Bool, error: Error?) {
  113. if !completed, error != nil {
  114. print("Unable to print: \(error?.localizedDescription ?? "unknown error")")
  115. }
  116. printing.onCompleted(printJob: self, completed: completed, error: error?.localizedDescription as NSString?)
  117. }
  118. public func printInteractionController(_ printInteractionController: UIPrintInteractionController, cutLengthFor paper: UIPrintPaper) -> CGFloat {
  119. if currentSize == nil{
  120. return paper.paperSize.height
  121. }
  122. return currentSize!.height
  123. }
  124. func printPdf(name: String, withPageSize size: CGSize, andMargin margin: CGRect, withPrinter printerID: String?, dynamically dyn: Bool) {
  125. currentSize = size
  126. dynamic = dyn
  127. let printing = UIPrintInteractionController.isPrintingAvailable
  128. if !printing {
  129. self.printing.onCompleted(printJob: self, completed: false, error: "Printing not available")
  130. return
  131. }
  132. if size.width > size.height {
  133. orientation = UIPrintInfo.Orientation.landscape
  134. }
  135. jobName = name
  136. printerName = printerID
  137. let controller = UIPrintInteractionController.shared
  138. controller.delegate = self
  139. let printInfo = UIPrintInfo.printInfo()
  140. printInfo.jobName = jobName!
  141. printInfo.outputType = .general
  142. if orientation != nil {
  143. printInfo.orientation = orientation!
  144. orientation = nil
  145. }
  146. controller.printInfo = printInfo
  147. controller.showsPaperSelectionForLoadedPapers = true
  148. controller.printPageRenderer = self
  149. if printerID != nil {
  150. let printerURL = URL(string: printerID!)
  151. if printerURL == nil {
  152. self.printing.onCompleted(printJob: self, completed: false, error: "Unable to find printer URL")
  153. return
  154. }
  155. let printer = UIPrinter(url: printerURL!)
  156. controller.print(to: printer, completionHandler: completionHandler)
  157. return
  158. }
  159. if dynamic {
  160. controller.present(animated: true, completionHandler: completionHandler)
  161. return
  162. }
  163. self.printing.onLayout(
  164. printJob: self,
  165. width: size.width,
  166. height: size.height,
  167. marginLeft: margin.minX,
  168. marginTop: margin.minY,
  169. marginRight: size.width - margin.maxX,
  170. marginBottom: size.height - margin.maxY
  171. )
  172. }
  173. static func sharePdf(data: Data, withSourceRect rect: CGRect, andName name: String, subject: String?, body: String?) {
  174. let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
  175. let fileURL = tmpDirURL.appendingPathComponent(name)
  176. do {
  177. try data.write(to: fileURL, options: .atomic)
  178. } catch {
  179. print("sharePdf error: \(error.localizedDescription)")
  180. return
  181. }
  182. let activityViewController = UIActivityViewController(activityItems: [fileURL, body as Any], applicationActivities: nil)
  183. activityViewController.setValue(subject, forKey: "subject")
  184. if UIDevice.current.userInterfaceIdiom == .pad {
  185. let controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
  186. activityViewController.popoverPresentationController?.sourceView = controller?.view
  187. activityViewController.popoverPresentationController?.sourceRect = rect
  188. }
  189. UIApplication.shared.keyWindow?.rootViewController?.present(activityViewController, animated: true)
  190. }
  191. func convertHtml(_ data: String, withPageSize rect: CGRect, andMargin margin: CGRect, andBaseUrl baseUrl: URL?) {
  192. let viewController = UIApplication.shared.delegate?.window?!.rootViewController
  193. let wkWebView = WKWebView(frame: viewController!.view.bounds)
  194. wkWebView.isHidden = true
  195. wkWebView.tag = 100
  196. viewController?.view.addSubview(wkWebView)
  197. wkWebView.loadHTMLString(data, baseURL: baseUrl ?? Bundle.main.bundleURL)
  198. urlObservation = wkWebView.observe(\.isLoading, changeHandler: { _, _ in
  199. // this is workaround for issue with loading local images
  200. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  201. // assign the print formatter to the print page renderer
  202. let renderer = UIPrintPageRenderer()
  203. renderer.addPrintFormatter(wkWebView.viewPrintFormatter(), startingAtPageAt: 0)
  204. // assign paperRect and printableRect values
  205. renderer.setValue(rect, forKey: "paperRect")
  206. renderer.setValue(margin, forKey: "printableRect")
  207. // create pdf context and draw each page
  208. let pdfData = NSMutableData()
  209. UIGraphicsBeginPDFContextToData(pdfData, rect, nil)
  210. for i in 0 ..< renderer.numberOfPages {
  211. UIGraphicsBeginPDFPage()
  212. renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
  213. }
  214. UIGraphicsEndPDFContext()
  215. if let viewWithTag = viewController?.view.viewWithTag(wkWebView.tag) {
  216. viewWithTag.removeFromSuperview() // remove hidden webview when pdf is generated
  217. // clear WKWebView cache
  218. if #available(iOS 9.0, *) {
  219. WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
  220. records.forEach { record in
  221. WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
  222. }
  223. }
  224. }
  225. }
  226. // dispose urlObservation
  227. self.urlObservation = nil
  228. self.printing.onHtmlRendered(printJob: self, pdfData: pdfData as Data)
  229. }
  230. })
  231. }
  232. static func pickPrinter(result: @escaping FlutterResult, withSourceRect rect: CGRect) {
  233. let controller = UIPrinterPickerController(initiallySelectedPrinter: nil)
  234. let pickPrinterCompletionHandler: UIPrinterPickerController.CompletionHandler = {
  235. (printerPickerController: UIPrinterPickerController, completed: Bool, error: Error?) in
  236. if !completed, error != nil {
  237. print("Unable to pick printer: \(error?.localizedDescription ?? "unknown error")")
  238. result(nil)
  239. return
  240. }
  241. if printerPickerController.selectedPrinter == nil {
  242. result(nil)
  243. return
  244. }
  245. let printer = printerPickerController.selectedPrinter!
  246. let data: NSDictionary = [
  247. "url": printer.url.absoluteString as Any,
  248. "name": printer.displayName as Any,
  249. "model": printer.makeAndModel as Any,
  250. "location": printer.displayLocation as Any,
  251. ]
  252. result(data)
  253. }
  254. if UIDevice.current.userInterfaceIdiom == .pad {
  255. let viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
  256. if viewController != nil {
  257. controller.present(from: rect, in: viewController!.view, animated: true, completionHandler: pickPrinterCompletionHandler)
  258. return
  259. }
  260. }
  261. controller.present(animated: true, completionHandler: pickPrinterCompletionHandler)
  262. }
  263. public func rasterPdf(data: Data, pages: [Int]?, scale: CGFloat) {
  264. let provider = CGDataProvider(data: data as CFData)!
  265. let document = CGPDFDocument(provider)
  266. if document == nil {
  267. printing.onPageRasterEnd(printJob: self, error: "Cannot raster a malformed PDF file")
  268. return
  269. }
  270. DispatchQueue.global().async {
  271. let pageCount = document!.numberOfPages
  272. for pageNum in pages ?? Array(0 ... pageCount - 1) {
  273. guard let page = document!.page(at: pageNum + 1) else { continue }
  274. let angle = CGFloat(page.rotationAngle) * CGFloat.pi / -180
  275. let rect = page.getBoxRect(.mediaBox)
  276. let width = Int(abs((cos(angle) * rect.width + sin(angle) * rect.height) * scale))
  277. let height = Int(abs((cos(angle) * rect.height + sin(angle) * rect.width) * scale))
  278. let stride = width * 4
  279. var data = Data(repeating: 0, count: stride * height)
  280. data.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) in
  281. let rgb = CGColorSpaceCreateDeviceRGB()
  282. let context = CGContext(
  283. data: outputBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
  284. width: width,
  285. height: height,
  286. bitsPerComponent: 8,
  287. bytesPerRow: stride,
  288. space: rgb,
  289. bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
  290. )
  291. if context != nil {
  292. context!.translateBy(x: CGFloat(width) / 2, y: CGFloat(height) / 2)
  293. context!.scaleBy(x: scale, y: scale)
  294. context!.rotate(by: angle)
  295. context!.translateBy(x: -rect.width / 2, y: -rect.height / 2)
  296. context!.drawPDFPage(page)
  297. }
  298. }
  299. DispatchQueue.main.sync {
  300. self.printing.onPageRasterized(printJob: self, imageData: data, width: width, height: height)
  301. }
  302. }
  303. DispatchQueue.main.sync {
  304. self.printing.onPageRasterEnd(printJob: self, error: nil)
  305. }
  306. }
  307. }
  308. public static func printingInfo() -> NSDictionary {
  309. let data: NSDictionary = [
  310. "directPrint": true,
  311. "dynamicLayout": true,
  312. "canPrint": true,
  313. "canConvertHtml": true,
  314. "canShare": true,
  315. "canRaster": true,
  316. "canListPrinters": false,
  317. ]
  318. return data
  319. }
  320. }