CameraPlugin.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. import Foundation
  2. import Capacitor
  3. import Photos
  4. import PhotosUI
  5. @objc(CAPCameraPlugin)
  6. public class CameraPlugin: CAPPlugin, CAPBridgedPlugin {
  7. public let identifier = "CAPCameraPlugin"
  8. public let jsName = "Camera"
  9. public let pluginMethods: [CAPPluginMethod] = [
  10. CAPPluginMethod(name: "getPhoto", returnType: CAPPluginReturnPromise),
  11. CAPPluginMethod(name: "pickImages", returnType: CAPPluginReturnPromise),
  12. CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
  13. CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
  14. CAPPluginMethod(name: "pickLimitedLibraryPhotos", returnType: CAPPluginReturnPromise),
  15. CAPPluginMethod(name: "getLimitedLibraryPhotos", returnType: CAPPluginReturnPromise)
  16. ]
  17. private var call: CAPPluginCall?
  18. private var settings = CameraSettings()
  19. private let defaultSource = CameraSource.prompt
  20. private let defaultDirection = CameraDirection.rear
  21. private var multiple = false
  22. private var imageCounter = 0
  23. @objc override public func checkPermissions(_ call: CAPPluginCall) {
  24. var result: [String: Any] = [:]
  25. for permission in CameraPermissionType.allCases {
  26. let state: String
  27. switch permission {
  28. case .camera:
  29. state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState
  30. case .photos:
  31. if #available(iOS 14, *) {
  32. state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState
  33. } else {
  34. state = PHPhotoLibrary.authorizationStatus().authorizationState
  35. }
  36. }
  37. result[permission.rawValue] = state
  38. }
  39. call.resolve(result)
  40. }
  41. @objc override public func requestPermissions(_ call: CAPPluginCall) {
  42. // get the list of desired types, if passed
  43. let typeList = call.getArray("permissions", String.self)?.compactMap({ (type) -> CameraPermissionType? in
  44. return CameraPermissionType(rawValue: type)
  45. }) ?? []
  46. // otherwise check everything
  47. let permissions: [CameraPermissionType] = (typeList.count > 0) ? typeList : CameraPermissionType.allCases
  48. // request the permissions
  49. let group = DispatchGroup()
  50. for permission in permissions {
  51. switch permission {
  52. case .camera:
  53. group.enter()
  54. AVCaptureDevice.requestAccess(for: .video) { _ in
  55. group.leave()
  56. }
  57. case .photos:
  58. group.enter()
  59. if #available(iOS 14, *) {
  60. PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
  61. group.leave()
  62. }
  63. } else {
  64. PHPhotoLibrary.requestAuthorization({ (_) in
  65. group.leave()
  66. })
  67. }
  68. }
  69. }
  70. group.notify(queue: DispatchQueue.main) { [weak self] in
  71. self?.checkPermissions(call)
  72. }
  73. }
  74. @objc func pickLimitedLibraryPhotos(_ call: CAPPluginCall) {
  75. if #available(iOS 14, *) {
  76. PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in
  77. if granted == .limited {
  78. if let viewController = self.bridge?.viewController {
  79. if #available(iOS 15, *) {
  80. PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { _ in
  81. self.getLimitedLibraryPhotos(call)
  82. }
  83. } else {
  84. PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)
  85. call.resolve([
  86. "photos": []
  87. ])
  88. }
  89. }
  90. } else {
  91. call.resolve([
  92. "photos": []
  93. ])
  94. }
  95. }
  96. } else {
  97. call.unavailable("Not available on iOS 13")
  98. }
  99. }
  100. @objc func getLimitedLibraryPhotos(_ call: CAPPluginCall) {
  101. if #available(iOS 14, *) {
  102. PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in
  103. if granted == .limited {
  104. self.call = call
  105. DispatchQueue.global(qos: .utility).async {
  106. let assets = PHAsset.fetchAssets(with: .image, options: nil)
  107. var processedImages: [ProcessedImage] = []
  108. let imageManager = PHImageManager.default()
  109. let options = PHImageRequestOptions()
  110. options.deliveryMode = .highQualityFormat
  111. let group = DispatchGroup()
  112. if assets.count > 0 {
  113. for index in 0...(assets.count - 1) {
  114. let asset = assets.object(at: index)
  115. let fullSize = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
  116. group.enter()
  117. imageManager.requestImage(for: asset, targetSize: fullSize, contentMode: .default, options: options) { image, _ in
  118. guard let image = image else {
  119. group.leave()
  120. return
  121. }
  122. processedImages.append(self.processedImage(from: image, with: asset.imageData))
  123. group.leave()
  124. }
  125. }
  126. }
  127. group.notify(queue: .global(qos: .utility)) { [weak self] in
  128. self?.returnImages(processedImages)
  129. }
  130. }
  131. } else {
  132. call.resolve([
  133. "photos": []
  134. ])
  135. }
  136. }
  137. } else {
  138. call.unavailable("Not available on iOS 13")
  139. }
  140. }
  141. @objc func getPhoto(_ call: CAPPluginCall) {
  142. self.multiple = false
  143. self.call = call
  144. self.settings = cameraSettings(from: call)
  145. // Make sure they have all the necessary info.plist settings
  146. if let missingUsageDescription = checkUsageDescriptions() {
  147. CAPLog.print("⚡️ ", self.pluginId, "-", missingUsageDescription)
  148. call.reject(missingUsageDescription)
  149. return
  150. }
  151. DispatchQueue.main.async {
  152. switch self.settings.source {
  153. case .prompt:
  154. self.showPrompt()
  155. case .camera:
  156. self.showCamera()
  157. case .photos:
  158. self.showPhotos()
  159. }
  160. }
  161. }
  162. @objc func pickImages(_ call: CAPPluginCall) {
  163. self.multiple = true
  164. self.call = call
  165. self.settings = cameraSettings(from: call)
  166. DispatchQueue.main.async {
  167. self.showPhotos()
  168. }
  169. }
  170. private func checkUsageDescriptions() -> String? {
  171. if let dict = Bundle.main.infoDictionary {
  172. for key in CameraPropertyListKeys.allCases where dict[key.rawValue] == nil {
  173. return key.missingMessage
  174. }
  175. }
  176. return nil
  177. }
  178. private func cameraSettings(from call: CAPPluginCall) -> CameraSettings {
  179. var settings = CameraSettings()
  180. settings.jpegQuality = min(abs(CGFloat(call.getFloat("quality") ?? 100.0)) / 100.0, 1.0)
  181. settings.allowEditing = call.getBool("allowEditing") ?? false
  182. settings.source = CameraSource(rawValue: call.getString("source") ?? defaultSource.rawValue) ?? defaultSource
  183. settings.direction = CameraDirection(rawValue: call.getString("direction") ?? defaultDirection.rawValue) ?? defaultDirection
  184. if let typeString = call.getString("resultType"), let type = CameraResultType(rawValue: typeString) {
  185. settings.resultType = type
  186. }
  187. settings.saveToGallery = call.getBool("saveToGallery") ?? false
  188. // Get the new image dimensions if provided
  189. settings.width = CGFloat(call.getInt("width") ?? 0)
  190. settings.height = CGFloat(call.getInt("height") ?? 0)
  191. if settings.width > 0 || settings.height > 0 {
  192. // We resize only if a dimension was provided
  193. settings.shouldResize = true
  194. }
  195. settings.shouldCorrectOrientation = call.getBool("correctOrientation") ?? true
  196. settings.userPromptText = CameraPromptText(title: call.getString("promptLabelHeader"),
  197. photoAction: call.getString("promptLabelPhoto"),
  198. cameraAction: call.getString("promptLabelPicture"),
  199. cancelAction: call.getString("promptLabelCancel"))
  200. if let styleString = call.getString("presentationStyle"), styleString == "popover" {
  201. settings.presentationStyle = .popover
  202. } else {
  203. settings.presentationStyle = .fullScreen
  204. }
  205. return settings
  206. }
  207. }
  208. // public delegate methods
  209. extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
  210. public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  211. picker.dismiss(animated: true)
  212. self.call?.reject("User cancelled photos app")
  213. }
  214. public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
  215. self.call?.reject("User cancelled photos app")
  216. }
  217. public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
  218. self.call?.reject("User cancelled photos app")
  219. }
  220. public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
  221. picker.dismiss(animated: true) {
  222. if let processedImage = self.processImage(from: info) {
  223. self.returnProcessedImage(processedImage)
  224. } else {
  225. self.call?.reject("Error processing image")
  226. }
  227. }
  228. }
  229. }
  230. @available(iOS 14, *)
  231. extension CameraPlugin: PHPickerViewControllerDelegate {
  232. public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
  233. picker.dismiss(animated: true, completion: nil)
  234. guard !results.isEmpty else {
  235. self.call?.reject("User cancelled photos app")
  236. return
  237. }
  238. self.fetchProcessedImages(from: results) { [weak self] processedImageArray in
  239. guard let processedImageArray else {
  240. self?.call?.reject("Error loading image")
  241. return
  242. }
  243. if self?.multiple == true {
  244. self?.returnImages(processedImageArray)
  245. } else if var processedImage = processedImageArray.first {
  246. processedImage.flags = .gallery
  247. self?.returnProcessedImage(processedImage)
  248. }
  249. }
  250. }
  251. private func fetchProcessedImages(from pickerResultArray: [PHPickerResult], accumulating: [ProcessedImage] = [], _ completionHandler: @escaping ([ProcessedImage]?) -> Void) {
  252. func loadImage(from pickerResult: PHPickerResult, _ completionHandler: @escaping (UIImage?) -> Void) {
  253. let itemProvider = pickerResult.itemProvider
  254. if itemProvider.canLoadObject(ofClass: UIImage.self) {
  255. // extract the image
  256. itemProvider.loadObject(ofClass: UIImage.self) { itemProviderReading, _ in
  257. completionHandler(itemProviderReading as? UIImage)
  258. }
  259. } else {
  260. // extract the image's data representation
  261. itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, _ in
  262. guard let data else {
  263. return completionHandler(nil)
  264. }
  265. completionHandler(UIImage(data: data))
  266. }
  267. }
  268. }
  269. guard let currentPickerResult = pickerResultArray.first else { return completionHandler(accumulating) }
  270. loadImage(from: currentPickerResult) { [weak self] loadedImage in
  271. guard let self, let loadedImage else { return completionHandler(nil) }
  272. var asset: PHAsset?
  273. if let assetId = currentPickerResult.assetIdentifier {
  274. asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject
  275. }
  276. let newElement = self.processedImage(from: loadedImage, with: asset?.imageData)
  277. self.fetchProcessedImages(
  278. from: Array(pickerResultArray.dropFirst()),
  279. accumulating: accumulating + [newElement],
  280. completionHandler
  281. )
  282. }
  283. }
  284. }
  285. private extension CameraPlugin {
  286. func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) {
  287. guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else {
  288. self.call?.reject("Unable to convert image to jpeg")
  289. return
  290. }
  291. if settings.resultType == CameraResultType.uri || multiple {
  292. guard let fileURL = try? saveTemporaryImage(jpeg),
  293. let webURL = bridge?.portablePath(fromLocalURL: fileURL) else {
  294. call?.reject("Unable to get portable path to file")
  295. return
  296. }
  297. if self.multiple {
  298. call?.resolve([
  299. "photos": [[
  300. "path": fileURL.absoluteString,
  301. "exif": processedImage.exifData,
  302. "webPath": webURL.absoluteString,
  303. "format": "jpeg"
  304. ]]
  305. ])
  306. return
  307. }
  308. call?.resolve([
  309. "path": fileURL.absoluteString,
  310. "exif": processedImage.exifData,
  311. "webPath": webURL.absoluteString,
  312. "format": "jpeg",
  313. "saved": isSaved
  314. ])
  315. } else if settings.resultType == CameraResultType.base64 {
  316. self.call?.resolve([
  317. "base64String": jpeg.base64EncodedString(),
  318. "exif": processedImage.exifData,
  319. "format": "jpeg",
  320. "saved": isSaved
  321. ])
  322. } else if settings.resultType == CameraResultType.dataURL {
  323. call?.resolve([
  324. "dataUrl": "data:image/jpeg;base64," + jpeg.base64EncodedString(),
  325. "exif": processedImage.exifData,
  326. "format": "jpeg",
  327. "saved": isSaved
  328. ])
  329. }
  330. }
  331. func returnImages(_ processedImages: [ProcessedImage]) {
  332. var photos: [PluginCallResultData] = []
  333. for processedImage in processedImages {
  334. guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else {
  335. self.call?.reject("Unable to convert image to jpeg")
  336. return
  337. }
  338. guard let fileURL = try? saveTemporaryImage(jpeg),
  339. let webURL = bridge?.portablePath(fromLocalURL: fileURL) else {
  340. call?.reject("Unable to get portable path to file")
  341. return
  342. }
  343. photos.append([
  344. "path": fileURL.absoluteString,
  345. "exif": processedImage.exifData,
  346. "webPath": webURL.absoluteString,
  347. "format": "jpeg"
  348. ])
  349. }
  350. call?.resolve([
  351. "photos": photos
  352. ])
  353. }
  354. func returnProcessedImage(_ processedImage: ProcessedImage) {
  355. // conditionally save the image
  356. if settings.saveToGallery && (processedImage.flags.contains(.edited) == true || processedImage.flags.contains(.gallery) == false) {
  357. _ = ImageSaver(image: processedImage.image) { error in
  358. var isSaved = false
  359. if error == nil {
  360. isSaved = true
  361. }
  362. self.returnImage(processedImage, isSaved: isSaved)
  363. }
  364. } else {
  365. self.returnImage(processedImage, isSaved: false)
  366. }
  367. }
  368. func showPrompt() {
  369. // Build the action sheet
  370. let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet)
  371. alert.addAction(UIAlertAction(title: settings.userPromptText.photoAction, style: .default, handler: { [weak self] (_: UIAlertAction) in
  372. self?.showPhotos()
  373. }))
  374. alert.addAction(UIAlertAction(title: settings.userPromptText.cameraAction, style: .default, handler: { [weak self] (_: UIAlertAction) in
  375. self?.showCamera()
  376. }))
  377. alert.addAction(UIAlertAction(title: settings.userPromptText.cancelAction, style: .cancel, handler: { [weak self] (_: UIAlertAction) in
  378. self?.call?.reject("User cancelled photos app")
  379. }))
  380. self.setCenteredPopover(alert)
  381. self.bridge?.viewController?.present(alert, animated: true, completion: nil)
  382. }
  383. func showCamera() {
  384. // check if we have a camera
  385. if (bridge?.isSimEnvironment ?? false) || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
  386. CAPLog.print("⚡️ ", self.pluginId, "-", "Camera not available in simulator")
  387. call?.reject("Camera not available while running in Simulator")
  388. return
  389. }
  390. // check for permission
  391. let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
  392. if authStatus == .restricted || authStatus == .denied {
  393. call?.reject("User denied access to camera")
  394. return
  395. }
  396. // we either already have permission or can prompt
  397. AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
  398. if granted {
  399. DispatchQueue.main.async {
  400. self?.presentCameraPicker()
  401. }
  402. } else {
  403. self?.call?.reject("User denied access to camera")
  404. }
  405. }
  406. }
  407. func showPhotos() {
  408. // check for permission
  409. let authStatus = PHPhotoLibrary.authorizationStatus()
  410. if authStatus == .restricted || authStatus == .denied {
  411. call?.reject("User denied access to photos")
  412. return
  413. }
  414. // we either already have permission or can prompt
  415. if authStatus == .authorized {
  416. presentSystemAppropriateImagePicker()
  417. } else {
  418. PHPhotoLibrary.requestAuthorization({ [weak self] (status) in
  419. if status == PHAuthorizationStatus.authorized {
  420. DispatchQueue.main.async { [weak self] in
  421. self?.presentSystemAppropriateImagePicker()
  422. }
  423. } else {
  424. self?.call?.reject("User denied access to photos")
  425. }
  426. })
  427. }
  428. }
  429. func presentCameraPicker() {
  430. let picker = UIImagePickerController()
  431. picker.delegate = self
  432. picker.allowsEditing = self.settings.allowEditing
  433. // select the input
  434. picker.sourceType = .camera
  435. if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) {
  436. picker.cameraDevice = .rear
  437. } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) {
  438. picker.cameraDevice = .front
  439. }
  440. // present
  441. picker.modalPresentationStyle = settings.presentationStyle
  442. if settings.presentationStyle == .popover {
  443. picker.popoverPresentationController?.delegate = self
  444. setCenteredPopover(picker)
  445. }
  446. bridge?.viewController?.present(picker, animated: true, completion: nil)
  447. }
  448. func presentSystemAppropriateImagePicker() {
  449. if #available(iOS 14, *) {
  450. presentPhotoPicker()
  451. } else {
  452. presentImagePicker()
  453. }
  454. }
  455. func presentImagePicker() {
  456. let picker = UIImagePickerController()
  457. picker.delegate = self
  458. picker.allowsEditing = self.settings.allowEditing
  459. // select the input
  460. picker.sourceType = .photoLibrary
  461. // present
  462. picker.modalPresentationStyle = settings.presentationStyle
  463. if settings.presentationStyle == .popover {
  464. picker.popoverPresentationController?.delegate = self
  465. setCenteredPopover(picker)
  466. }
  467. bridge?.viewController?.present(picker, animated: true, completion: nil)
  468. }
  469. @available(iOS 14, *)
  470. func presentPhotoPicker() {
  471. var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
  472. configuration.selectionLimit = self.multiple ? (self.call?.getInt("limit") ?? 0) : 1
  473. configuration.filter = .images
  474. let picker = PHPickerViewController(configuration: configuration)
  475. picker.delegate = self
  476. // present
  477. picker.modalPresentationStyle = settings.presentationStyle
  478. if settings.presentationStyle == .popover {
  479. picker.popoverPresentationController?.delegate = self
  480. setCenteredPopover(picker)
  481. }
  482. bridge?.viewController?.present(picker, animated: true, completion: nil)
  483. }
  484. func saveTemporaryImage(_ data: Data) throws -> URL {
  485. var url: URL
  486. repeat {
  487. imageCounter += 1
  488. url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("photo-\(imageCounter).jpg")
  489. } while FileManager.default.fileExists(atPath: url.path)
  490. try data.write(to: url, options: .atomic)
  491. return url
  492. }
  493. func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? {
  494. var selectedImage: UIImage?
  495. var flags: PhotoFlags = []
  496. // get the image
  497. if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
  498. selectedImage = edited // use the edited version
  499. flags = flags.union([.edited])
  500. } else if let original = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
  501. selectedImage = original // use the original version
  502. }
  503. guard let image = selectedImage else {
  504. return nil
  505. }
  506. var metadata: [String: Any] = [:]
  507. // get the image's metadata from the picker or from the photo album
  508. if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] {
  509. metadata = photoMetadata
  510. } else {
  511. flags = flags.union([.gallery])
  512. }
  513. if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset {
  514. metadata = asset.imageData
  515. }
  516. // get the result
  517. var result = processedImage(from: image, with: metadata)
  518. result.flags = flags
  519. return result
  520. }
  521. func processedImage(from image: UIImage, with metadata: [String: Any]?) -> ProcessedImage {
  522. var result = ProcessedImage(image: image, metadata: metadata ?? [:])
  523. // resizing the image only makes sense if we have real values to which to constrain it
  524. if settings.shouldResize, settings.width > 0 || settings.height > 0 {
  525. result.image = result.image.reformat(to: CGSize(width: settings.width, height: settings.height))
  526. result.overwriteMetadataOrientation(to: 1)
  527. } else if settings.shouldCorrectOrientation {
  528. // resizing implicitly reformats the image so this is only needed if we aren't resizing
  529. result.image = result.image.reformat()
  530. result.overwriteMetadataOrientation(to: 1)
  531. }
  532. return result
  533. }
  534. }