From c34afb7187036bcab65efbe5585f2f59dd446c37 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Fri, 29 Sep 2023 09:56:14 -0300 Subject: [PATCH] license headers --- .scripts/ci/check-license-header.js | 11 +- .../tauri/camera/ExampleInstrumentedTest.kt | 4 + .../java/CameraBottomSheetDialogFragment.kt | 4 + .../android/src/main/java/CameraPlugin.kt | 4 + .../android/src/main/java/CameraUtils.kt | 4 + .../android/src/main/java/ExifWrapper.kt | 4 + .../android/src/main/java/ImageUtils.kt | 4 + .../java/app/tauri/camera/ExampleUnitTest.kt | 4 + plugins/camera/dist-js/index.d.ts | 4 + plugins/camera/ios/Package.swift | 51 +- .../camera/ios/Sources/CameraExtensions.swift | 169 +-- plugins/camera/ios/Sources/CameraPlugin.swift | 1060 +++++++++-------- plugins/camera/ios/Sources/CameraTypes.swift | 4 + plugins/camera/ios/Sources/ImageSaver.swift | 4 + .../ios/Tests/PluginTests/PluginTests.swift | 5 + 15 files changed, 720 insertions(+), 616 deletions(-) diff --git a/.scripts/ci/check-license-header.js b/.scripts/ci/check-license-header.js index a322957d..1645001e 100644 --- a/.scripts/ci/check-license-header.js +++ b/.scripts/ci/check-license-header.js @@ -27,7 +27,7 @@ const ignore = [ async function checkFile(file) { if ( extensions.some((e) => file.endsWith(e)) && - !ignore.some((i) => file.endsWith(i)) + !ignore.some((i) => file.includes(i)) ) { const fileStream = fs.createReadStream(file); const rl = readline.createInterface({ @@ -72,8 +72,8 @@ async function check(src) { const missingHeader = []; for (const entry of fs.readdirSync(src, { - withFileTypes: true, - })) { + withFileTypes: true, + })) { const p = path.join(src, entry.name); if (entry.isSymbolicLink() || ignore.includes(entry.name)) { @@ -113,7 +113,8 @@ if (files.length > 0) { run(); } else { - check(path.resolve(new URL(import.meta.url).pathname, "../../..")).then( + check(path.resolve(new URL( + import.meta.url).pathname, "../../..")).then( (missing) => { if (missing.length > 0) { console.log(missing.join("\n")); @@ -121,4 +122,4 @@ if (files.length > 0) { } }, ); -} +} \ No newline at end of file diff --git a/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt b/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt index 5f729a50..a2767ffe 100644 --- a/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt +++ b/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import androidx.test.platform.app.InstrumentationRegistry diff --git a/plugins/camera/android/src/main/java/CameraBottomSheetDialogFragment.kt b/plugins/camera/android/src/main/java/CameraBottomSheetDialogFragment.kt index 655723b1..cfed64fd 100644 --- a/plugins/camera/android/src/main/java/CameraBottomSheetDialogFragment.kt +++ b/plugins/camera/android/src/main/java/CameraBottomSheetDialogFragment.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import android.annotation.SuppressLint diff --git a/plugins/camera/android/src/main/java/CameraPlugin.kt b/plugins/camera/android/src/main/java/CameraPlugin.kt index 3a8b466d..51db4007 100644 --- a/plugins/camera/android/src/main/java/CameraPlugin.kt +++ b/plugins/camera/android/src/main/java/CameraPlugin.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import android.Manifest diff --git a/plugins/camera/android/src/main/java/CameraUtils.kt b/plugins/camera/android/src/main/java/CameraUtils.kt index 76972e3d..30eb9295 100644 --- a/plugins/camera/android/src/main/java/CameraUtils.kt +++ b/plugins/camera/android/src/main/java/CameraUtils.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import android.app.Activity diff --git a/plugins/camera/android/src/main/java/ExifWrapper.kt b/plugins/camera/android/src/main/java/ExifWrapper.kt index 1ef69ef4..9f9d41ac 100644 --- a/plugins/camera/android/src/main/java/ExifWrapper.kt +++ b/plugins/camera/android/src/main/java/ExifWrapper.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import androidx.exifinterface.media.ExifInterface.* diff --git a/plugins/camera/android/src/main/java/ImageUtils.kt b/plugins/camera/android/src/main/java/ImageUtils.kt index b6369cf2..3522dd34 100644 --- a/plugins/camera/android/src/main/java/ImageUtils.kt +++ b/plugins/camera/android/src/main/java/ImageUtils.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import android.content.Context diff --git a/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt b/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt index 7547c0b3..97f9ae03 100644 --- a/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt +++ b/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + package app.tauri.camera import org.junit.Test diff --git a/plugins/camera/dist-js/index.d.ts b/plugins/camera/dist-js/index.d.ts index 426991e3..3a966e14 100644 --- a/plugins/camera/dist-js/index.d.ts +++ b/plugins/camera/dist-js/index.d.ts @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + export declare enum Source { Prompt = "PROMPT", Camera = "CAMERA", diff --git a/plugins/camera/ios/Package.swift b/plugins/camera/ios/Package.swift index d5025e09..a5e0dcc4 100644 --- a/plugins/camera/ios/Package.swift +++ b/plugins/camera/ios/Package.swift @@ -1,31 +1,34 @@ // swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "tauri-plugin-camera", - platforms: [ - .iOS(.v11), - ], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "tauri-plugin-camera", - type: .static, - targets: ["tauri-plugin-camera"]), - ], - dependencies: [ - .package(name: "Tauri", path: "../.tauri/tauri-api") - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "tauri-plugin-camera", - dependencies: [ - .byName(name: "Tauri") - ], - path: "Sources") - ] + name: "tauri-plugin-camera", + platforms: [ + .iOS(.v11) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-camera", + type: .static, + targets: ["tauri-plugin-camera"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-camera", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] ) diff --git a/plugins/camera/ios/Sources/CameraExtensions.swift b/plugins/camera/ios/Sources/CameraExtensions.swift index 938b61c0..5390dc05 100644 --- a/plugins/camera/ios/Sources/CameraExtensions.swift +++ b/plugins/camera/ios/Sources/CameraExtensions.swift @@ -1,105 +1,112 @@ -import UIKit +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + import Photos +import UIKit internal protocol CameraAuthorizationState { - var authorizationState: String { get } + var authorizationState: String { get } } extension AVAuthorizationStatus: CameraAuthorizationState { - var authorizationState: String { - switch self { - case .denied, .restricted: - return "denied" - case .authorized: - return "granted" - case .notDetermined: - fallthrough - @unknown default: - return "prompt" - } + var authorizationState: String { + switch self { + case .denied, .restricted: + return "denied" + case .authorized: + return "granted" + case .notDetermined: + fallthrough + @unknown default: + return "prompt" } + } } extension PHAuthorizationStatus: CameraAuthorizationState { - var authorizationState: String { - switch self { - case .denied, .restricted: - return "denied" - case .authorized: - return "granted" - #if swift(>=5.3) - // poor proxy for Xcode 12/iOS 14, should be removed once building with Xcode 12 is required - case .limited: - return "limited" - #endif - case .notDetermined: - fallthrough - @unknown default: - return "prompt" - } + var authorizationState: String { + switch self { + case .denied, .restricted: + return "denied" + case .authorized: + return "granted" + #if swift(>=5.3) + // poor proxy for Xcode 12/iOS 14, should be removed once building with Xcode 12 is required + case .limited: + return "limited" + #endif + case .notDetermined: + fallthrough + @unknown default: + return "prompt" } + } } -internal extension PHAsset { - /** +extension PHAsset { + /** Retrieves the image metadata for the asset. */ - var imageData: [String: Any] { - let options = PHImageRequestOptions() - options.isSynchronous = true - options.resizeMode = .none - options.isNetworkAccessAllowed = false - options.version = .current + var imageData: [String: Any] { + let options = PHImageRequestOptions() + options.isSynchronous = true + options.resizeMode = .none + options.isNetworkAccessAllowed = false + options.version = .current - var result: [String: Any] = [:] - _ = PHCachingImageManager().requestImageDataAndOrientation(for: self, options: options) { (data, _, _, _) in - if let data = data as NSData? { - let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse] as CFDictionary - if let imgSrc = CGImageSourceCreateWithData(data, options), - let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options) as? [String: Any] { - result = metadata - } - } + var result: [String: Any] = [:] + _ = PHCachingImageManager().requestImageDataAndOrientation(for: self, options: options) { + (data, _, _, _) in + if let data = data as NSData? { + let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse] as CFDictionary + if let imgSrc = CGImageSourceCreateWithData(data, options), + let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options) as? [String: Any] + { + result = metadata } - return result + } } + return result + } } -internal extension UIImage { - /** +extension UIImage { + /** Generates a new image from the existing one, implicitly resetting any orientation. Dimensions greater than 0 will resize the image while preserving the aspect ratio. */ - func reformat(to size: CGSize? = nil) -> UIImage { - let imageHeight = self.size.height - let imageWidth = self.size.width - // determine the max dimensions, 0 is treated as 'no restriction' - var maxWidth: CGFloat - if let size = size, size.width > 0 { - maxWidth = size.width - } else { - maxWidth = imageWidth - } - let maxHeight: CGFloat - if let size = size, size.height > 0 { - maxHeight = size.height - } else { - maxHeight = imageHeight - } - // adjust to preserve aspect ratio - var targetWidth = min(imageWidth, maxWidth) - var targetHeight = (imageHeight * targetWidth) / imageWidth - if targetHeight > maxHeight { - targetWidth = (imageWidth * maxHeight) / imageHeight - targetHeight = maxHeight - } - // generate the new image and return - let format: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat.default() - format.scale = 1.0 - format.opaque = false - let renderer = UIGraphicsImageRenderer(size: CGSize(width: targetWidth, height: targetHeight), format: format) - return renderer.image { (_) in - self.draw(in: CGRect(origin: .zero, size: CGSize(width: targetWidth, height: targetHeight))) - } + func reformat(to size: CGSize? = nil) -> UIImage { + let imageHeight = self.size.height + let imageWidth = self.size.width + // determine the max dimensions, 0 is treated as 'no restriction' + var maxWidth: CGFloat + if let size = size, size.width > 0 { + maxWidth = size.width + } else { + maxWidth = imageWidth + } + let maxHeight: CGFloat + if let size = size, size.height > 0 { + maxHeight = size.height + } else { + maxHeight = imageHeight } -} \ No newline at end of file + // adjust to preserve aspect ratio + var targetWidth = min(imageWidth, maxWidth) + var targetHeight = (imageHeight * targetWidth) / imageWidth + if targetHeight > maxHeight { + targetWidth = (imageWidth * maxHeight) / imageHeight + targetHeight = maxHeight + } + // generate the new image and return + let format: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat.default() + format.scale = 1.0 + format.opaque = false + let renderer = UIGraphicsImageRenderer( + size: CGSize(width: targetWidth, height: targetHeight), format: format) + return renderer.image { (_) in + self.draw(in: CGRect(origin: .zero, size: CGSize(width: targetWidth, height: targetHeight))) + } + } +} diff --git a/plugins/camera/ios/Sources/CameraPlugin.swift b/plugins/camera/ios/Sources/CameraPlugin.swift index 82154c2a..555d135a 100644 --- a/plugins/camera/ios/Sources/CameraPlugin.swift +++ b/plugins/camera/ios/Sources/CameraPlugin.swift @@ -1,573 +1,621 @@ -import UIKit -import WebKit -import Tauri +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + import Photos import PhotosUI +import Tauri +import UIKit +import WebKit public class CameraPlugin: Plugin { - private var invoke: Invoke? - private var settings = CameraSettings() - private let defaultSource = CameraSource.prompt - private let defaultDirection = CameraDirection.rear - private var multiple = false - - private var imageCounter = 0 - - @objc override public func checkPermissions(_ invoke: Invoke) { - var result: [String: Any] = [:] - for permission in CameraPermissionType.allCases { - let state: String - switch permission { - case .camera: - state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState - case .photos: - if #available(iOS 14, *) { - state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState - } else { - state = PHPhotoLibrary.authorizationStatus().authorizationState - } - } - result[permission.rawValue] = state - } - invoke.resolve(result) - } - - @objc override public func requestPermissions(_ invoke: Invoke) { - // get the list of desired types, if passed - let typeList = invoke.getArray("permissions", String.self)?.compactMap({ (type) -> CameraPermissionType? in - return CameraPermissionType(rawValue: type) - }) ?? [] - // otherwise check everything - let permissions: [CameraPermissionType] = (typeList.count > 0) ? typeList : CameraPermissionType.allCases - // request the permissions - let group = DispatchGroup() - for permission in permissions { - switch permission { - case .camera: - group.enter() - AVCaptureDevice.requestAccess(for: .video) { _ in - group.leave() - } - case .photos: - group.enter() - if #available(iOS 14, *) { - PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in - group.leave() - } - } else { - PHPhotoLibrary.requestAuthorization({ (_) in - group.leave() - }) - } - } - } - group.notify(queue: DispatchQueue.main) { [weak self] in - self?.checkPermissions(invoke) + private var invoke: Invoke? + private var settings = CameraSettings() + private let defaultSource = CameraSource.prompt + private let defaultDirection = CameraDirection.rear + private var multiple = false + + private var imageCounter = 0 + + @objc override public func checkPermissions(_ invoke: Invoke) { + var result: [String: Any] = [:] + for permission in CameraPermissionType.allCases { + let state: String + switch permission { + case .camera: + state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState + case .photos: + if #available(iOS 14, *) { + state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState + } else { + state = PHPhotoLibrary.authorizationStatus().authorizationState } + } + result[permission.rawValue] = state } - - @objc func pickLimitedLibraryPhotos(_ invoke: Invoke) { + invoke.resolve(result) + } + + @objc override public func requestPermissions(_ invoke: Invoke) { + // get the list of desired types, if passed + let typeList = + invoke.getArray("permissions", String.self)?.compactMap({ (type) -> CameraPermissionType? in + return CameraPermissionType(rawValue: type) + }) ?? [] + // otherwise check everything + let permissions: [CameraPermissionType] = + (typeList.count > 0) ? typeList : CameraPermissionType.allCases + // request the permissions + let group = DispatchGroup() + for permission in permissions { + switch permission { + case .camera: + group.enter() + AVCaptureDevice.requestAccess(for: .video) { _ in + group.leave() + } + case .photos: + group.enter() if #available(iOS 14, *) { - PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in - if granted == .limited { - if let viewController = self.manager.viewController { - if #available(iOS 15, *) { - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { _ in - self.getLimitedLibraryPhotos(invoke) - } - } else { - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) - invoke.resolve([ - "photos": [] - ]) - } - } - } else { - invoke.resolve([ - "photos": [] - ]) - } - } + PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in + group.leave() + } } else { - invoke.unavailable("Not available on iOS 13") + PHPhotoLibrary.requestAuthorization({ (_) in + group.leave() + }) } + } } - - @objc func getLimitedLibraryPhotos(_ invoke: Invoke) { - if #available(iOS 14, *) { - PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in - if granted == .limited { - - self.invoke = invoke - - DispatchQueue.global(qos: .utility).async { - let assets = PHAsset.fetchAssets(with: .image, options: nil) - var processedImages: [ProcessedImage] = [] - - let imageManager = PHImageManager.default() - let options = PHImageRequestOptions() - options.deliveryMode = .highQualityFormat - - let group = DispatchGroup() - - for index in 0...(assets.count - 1) { - let asset = assets.object(at: index) - let fullSize = CGSize(width: asset.pixelWidth, height: asset.pixelHeight) - - group.enter() - imageManager.requestImage(for: asset, targetSize: fullSize, contentMode: .default, options: options) { image, _ in - guard let image = image else { - group.leave() - return - } - processedImages.append(self.processedImage(from: image, with: asset.imageData)) - group.leave() - } - } - - group.notify(queue: .global(qos: .utility)) { [weak self] in - self?.returnImages(processedImages) - } - } - } else { - invoke.resolve([ - "photos": [] - ]) - } + group.notify(queue: DispatchQueue.main) { [weak self] in + self?.checkPermissions(invoke) + } + } + + @objc func pickLimitedLibraryPhotos(_ invoke: Invoke) { + if #available(iOS 14, *) { + PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in + if granted == .limited { + if let viewController = self.manager.viewController { + if #available(iOS 15, *) { + PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { _ in + self.getLimitedLibraryPhotos(invoke) + } + } else { + PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) + invoke.resolve([ + "photos": [] + ]) } + } } else { - invoke.unavailable("Not available on iOS 13") + invoke.resolve([ + "photos": [] + ]) } + } + } else { + invoke.unavailable("Not available on iOS 13") } + } - @objc func getPhoto(_ invoke: Invoke) { - self.multiple = false - self.invoke = invoke - self.settings = cameraSettings(from: invoke) + @objc func getLimitedLibraryPhotos(_ invoke: Invoke) { + if #available(iOS 14, *) { + PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in + if granted == .limited { - // Make sure they have all the necessary info.plist settings - if let missingUsageDescription = checkUsageDescriptions() { - Logger.error("[PLUGIN]", "Camera", "-", missingUsageDescription) - invoke.reject(missingUsageDescription) - return - } + self.invoke = invoke - DispatchQueue.main.async { - switch self.settings.source { - case .prompt: - self.showPrompt() - case .camera: - self.showCamera() - case .photos: - self.showPhotos() - } - } - } + DispatchQueue.global(qos: .utility).async { + let assets = PHAsset.fetchAssets(with: .image, options: nil) + var processedImages: [ProcessedImage] = [] - @objc func pickImages(_ invoke: Invoke) { - self.multiple = true - self.invoke = invoke - self.settings = cameraSettings(from: invoke) - DispatchQueue.main.async { - self.showPhotos() - } - } + let imageManager = PHImageManager.default() + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + + let group = DispatchGroup() + + for index in 0...(assets.count - 1) { + let asset = assets.object(at: index) + let fullSize = CGSize(width: asset.pixelWidth, height: asset.pixelHeight) - private func checkUsageDescriptions() -> String? { - if let dict = Bundle.main.infoDictionary { - for key in CameraPropertyListKeys.allCases where dict[key.rawValue] == nil { - return key.missingMessage + group.enter() + imageManager.requestImage( + for: asset, targetSize: fullSize, contentMode: .default, options: options + ) { image, _ in + guard let image = image else { + group.leave() + return + } + processedImages.append(self.processedImage(from: image, with: asset.imageData)) + group.leave() + } } - } - return nil - } - private func cameraSettings(from invoke: Invoke) -> CameraSettings { - var settings = CameraSettings() - settings.jpegQuality = min(abs(CGFloat(invoke.getFloat("quality") ?? 100.0)) / 100.0, 1.0) - settings.allowEditing = invoke.getBool("allowEditing") ?? false - settings.source = CameraSource(rawValue: invoke.getString("source") ?? defaultSource.rawValue) ?? defaultSource - settings.direction = CameraDirection(rawValue: invoke.getString("direction") ?? defaultDirection.rawValue) ?? defaultDirection - if let typeString = invoke.getString("resultType"), let type = CameraResultType(rawValue: typeString) { - settings.resultType = type - } - settings.saveToGallery = invoke.getBool("saveToGallery") ?? false - - // Get the new image dimensions if provided - settings.width = CGFloat(invoke.getInt("width") ?? 0) - settings.height = CGFloat(invoke.getInt("height") ?? 0) - if settings.width > 0 || settings.height > 0 { - // We resize only if a dimension was provided - settings.shouldResize = true - } - settings.shouldCorrectOrientation = invoke.getBool("correctOrientation") ?? true - settings.userPromptText = CameraPromptText(title: invoke.getString("promptLabelHeader"), - photoAction: invoke.getString("promptLabelPhoto"), - cameraAction: invoke.getString("promptLabelPicture"), - cancelAction: invoke.getString("promptLabelCancel")) - if let styleString = invoke.getString("presentationStyle"), styleString == "popover" { - settings.presentationStyle = .popover + group.notify(queue: .global(qos: .utility)) { [weak self] in + self?.returnImages(processedImages) + } + } } else { - settings.presentationStyle = .fullScreen + invoke.resolve([ + "photos": [] + ]) } - - return settings + } + } else { + invoke.unavailable("Not available on iOS 13") } -} - -// public delegate methods -extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate { - public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - picker.dismiss(animated: true) - self.invoke?.reject("User cancelled photos app") + } + + @objc func getPhoto(_ invoke: Invoke) { + self.multiple = false + self.invoke = invoke + self.settings = cameraSettings(from: invoke) + + // Make sure they have all the necessary info.plist settings + if let missingUsageDescription = checkUsageDescriptions() { + Logger.error("[PLUGIN]", "Camera", "-", missingUsageDescription) + invoke.reject(missingUsageDescription) + return } - public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { - self.invoke?.reject("User cancelled photos app") + DispatchQueue.main.async { + switch self.settings.source { + case .prompt: + self.showPrompt() + case .camera: + self.showCamera() + case .photos: + self.showPhotos() + } + } + } + + @objc func pickImages(_ invoke: Invoke) { + self.multiple = true + self.invoke = invoke + self.settings = cameraSettings(from: invoke) + DispatchQueue.main.async { + self.showPhotos() } + } - public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - self.invoke?.reject("User cancelled photos app") + private func checkUsageDescriptions() -> String? { + if let dict = Bundle.main.infoDictionary { + for key in CameraPropertyListKeys.allCases where dict[key.rawValue] == nil { + return key.missingMessage + } + } + return nil + } + + private func cameraSettings(from invoke: Invoke) -> CameraSettings { + var settings = CameraSettings() + settings.jpegQuality = min(abs(CGFloat(invoke.getFloat("quality") ?? 100.0)) / 100.0, 1.0) + settings.allowEditing = invoke.getBool("allowEditing") ?? false + settings.source = + CameraSource(rawValue: invoke.getString("source") ?? defaultSource.rawValue) ?? defaultSource + settings.direction = + CameraDirection(rawValue: invoke.getString("direction") ?? defaultDirection.rawValue) + ?? defaultDirection + if let typeString = invoke.getString("resultType"), + let type = CameraResultType(rawValue: typeString) + { + settings.resultType = type + } + settings.saveToGallery = invoke.getBool("saveToGallery") ?? false + + // Get the new image dimensions if provided + settings.width = CGFloat(invoke.getInt("width") ?? 0) + settings.height = CGFloat(invoke.getInt("height") ?? 0) + if settings.width > 0 || settings.height > 0 { + // We resize only if a dimension was provided + settings.shouldResize = true + } + settings.shouldCorrectOrientation = invoke.getBool("correctOrientation") ?? true + settings.userPromptText = CameraPromptText( + title: invoke.getString("promptLabelHeader"), + photoAction: invoke.getString("promptLabelPhoto"), + cameraAction: invoke.getString("promptLabelPicture"), + cancelAction: invoke.getString("promptLabelCancel")) + if let styleString = invoke.getString("presentationStyle"), styleString == "popover" { + settings.presentationStyle = .popover + } else { + settings.presentationStyle = .fullScreen } - public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - picker.dismiss(animated: true) { - if let processedImage = self.processImage(from: info) { - self.returnProcessedImage(processedImage) - } else { - self.invoke?.reject("Error processing image") - } - } + return settings + } +} + +// public delegate methods +extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, + UIPopoverPresentationControllerDelegate +{ + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + self.invoke?.reject("User cancelled photos app") + } + + public func popoverPresentationControllerDidDismissPopover( + _ popoverPresentationController: UIPopoverPresentationController + ) { + self.invoke?.reject("User cancelled photos app") + } + + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.invoke?.reject("User cancelled photos app") + } + + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { + picker.dismiss(animated: true) { + if let processedImage = self.processImage(from: info) { + self.returnProcessedImage(processedImage) + } else { + self.invoke?.reject("Error processing image") + } } + } } @available(iOS 14, *) extension CameraPlugin: PHPickerViewControllerDelegate { - public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true, completion: nil) - guard let result = results.first else { - self.invoke?.reject("User cancelled photos app") - return - } - if multiple { - var images: [ProcessedImage] = [] - var processedCount = 0 - for img in results { - guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else { - self.invoke?.reject("Error loading image") - return - } - // extract the image - img.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in - if let image = reading as? UIImage { - var asset: PHAsset? - if let assetId = img.assetIdentifier { - asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject - } - if let processedImage = self?.processedImage(from: image, with: asset?.imageData) { - images.append(processedImage) - } - processedCount += 1 - if processedCount == results.count { - self?.returnImages(images) - } - } else { - self?.invoke?.reject("Error loading image") - } - } + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true, completion: nil) + guard let result = results.first else { + self.invoke?.reject("User cancelled photos app") + return + } + if multiple { + var images: [ProcessedImage] = [] + var processedCount = 0 + for img in results { + guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else { + self.invoke?.reject("Error loading image") + return + } + // extract the image + img.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in + if let image = reading as? UIImage { + var asset: PHAsset? + if let assetId = img.assetIdentifier { + asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject } - - } else { - guard result.itemProvider.canLoadObject(ofClass: UIImage.self) else { - self.invoke?.reject("Error loading image") - return + if let processedImage = self?.processedImage(from: image, with: asset?.imageData) { + images.append(processedImage) } - // extract the image - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in - if let image = reading as? UIImage { - var asset: PHAsset? - if let assetId = result.assetIdentifier { - asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject - } - if var processedImage = self?.processedImage(from: image, with: asset?.imageData) { - processedImage.flags = .gallery - self?.returnProcessedImage(processedImage) - return - } - } - self?.invoke?.reject("Error loading image") + processedCount += 1 + if processedCount == results.count { + self?.returnImages(images) } + } else { + self?.invoke?.reject("Error loading image") + } + } + } + + } else { + guard result.itemProvider.canLoadObject(ofClass: UIImage.self) else { + self.invoke?.reject("Error loading image") + return + } + // extract the image + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in + if let image = reading as? UIImage { + var asset: PHAsset? + if let assetId = result.assetIdentifier { + asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject + } + if var processedImage = self?.processedImage(from: image, with: asset?.imageData) { + processedImage.flags = .gallery + self?.returnProcessedImage(processedImage) + return + } } + self?.invoke?.reject("Error loading image") + } } + } } -private extension CameraPlugin { - func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) { - guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { - self.invoke?.reject("Unable to convert image to jpeg") - return - } - - if settings.resultType == CameraResultType.uri || multiple { - guard let fileURL = try? saveTemporaryImage(jpeg), - let webURL = manager.assetUrl(fromLocalURL: fileURL) else { - invoke?.reject("Unable to get asset URL to file") - return - } - if self.multiple { - invoke?.resolve([ - "photos": [[ - "data": fileURL.absoluteString, - "exif": processedImage.exifData, - "assetUrl": webURL.absoluteString, - "format": "jpeg" - ]] - ]) - return - } - invoke?.resolve([ - "data": fileURL.absoluteString, - "exif": processedImage.exifData, - "assetUrl": webURL.absoluteString, - "format": "jpeg", - "saved": isSaved - ]) - } else if settings.resultType == CameraResultType.base64 { - self.invoke?.resolve([ - "data": jpeg.base64EncodedString(), - "exif": processedImage.exifData, - "format": "jpeg", - "saved": isSaved - ]) - } else if settings.resultType == CameraResultType.dataURL { - invoke?.resolve([ - "data": "data:image/jpeg;base64," + jpeg.base64EncodedString(), - "exif": processedImage.exifData, - "format": "jpeg", - "saved": isSaved - ]) - } +extension CameraPlugin { + fileprivate func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) { + guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { + self.invoke?.reject("Unable to convert image to jpeg") + return } - func returnImages(_ processedImages: [ProcessedImage]) { - var photos: [JsonObject] = [] - for processedImage in processedImages { - guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { - self.invoke?.reject("Unable to convert image to jpeg") - return - } - - guard let fileURL = try? saveTemporaryImage(jpeg), - let webURL = manager.assetUrl(fromLocalURL: fileURL) else { - invoke?.reject("Unable to get asset URL to file") - return - } - - photos.append([ - "path": fileURL.absoluteString, - "exif": processedImage.exifData, - "assetUrl": webURL.absoluteString, - "format": "jpeg" - ]) - } + if settings.resultType == CameraResultType.uri || multiple { + guard let fileURL = try? saveTemporaryImage(jpeg), + let webURL = manager.assetUrl(fromLocalURL: fileURL) + else { + invoke?.reject("Unable to get asset URL to file") + return + } + if self.multiple { invoke?.resolve([ - "photos": photos + "photos": [ + [ + "data": fileURL.absoluteString, + "exif": processedImage.exifData, + "assetUrl": webURL.absoluteString, + "format": "jpeg", + ] + ] ]) + return + } + invoke?.resolve([ + "data": fileURL.absoluteString, + "exif": processedImage.exifData, + "assetUrl": webURL.absoluteString, + "format": "jpeg", + "saved": isSaved, + ]) + } else if settings.resultType == CameraResultType.base64 { + self.invoke?.resolve([ + "data": jpeg.base64EncodedString(), + "exif": processedImage.exifData, + "format": "jpeg", + "saved": isSaved, + ]) + } else if settings.resultType == CameraResultType.dataURL { + invoke?.resolve([ + "data": "data:image/jpeg;base64," + jpeg.base64EncodedString(), + "exif": processedImage.exifData, + "format": "jpeg", + "saved": isSaved, + ]) } - - func returnProcessedImage(_ processedImage: ProcessedImage) { - // conditionally save the image - if settings.saveToGallery && (processedImage.flags.contains(.edited) == true || processedImage.flags.contains(.gallery) == false) { - _ = ImageSaver(image: processedImage.image) { error in - var isSaved = false - if error == nil { - isSaved = true - } - self.returnImage(processedImage, isSaved: isSaved) - } - } else { - self.returnImage(processedImage, isSaved: false) - } + } + + fileprivate func returnImages(_ processedImages: [ProcessedImage]) { + var photos: [JsonObject] = [] + for processedImage in processedImages { + guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else { + self.invoke?.reject("Unable to convert image to jpeg") + return + } + + guard let fileURL = try? saveTemporaryImage(jpeg), + let webURL = manager.assetUrl(fromLocalURL: fileURL) + else { + invoke?.reject("Unable to get asset URL to file") + return + } + + photos.append([ + "path": fileURL.absoluteString, + "exif": processedImage.exifData, + "assetUrl": webURL.absoluteString, + "format": "jpeg", + ]) } - - func showPrompt() { - // Build the action sheet - let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet) - alert.addAction(UIAlertAction(title: settings.userPromptText.photoAction, style: .default, handler: { [weak self] (_: UIAlertAction) in - self?.showPhotos() + invoke?.resolve([ + "photos": photos + ]) + } + + fileprivate func returnProcessedImage(_ processedImage: ProcessedImage) { + // conditionally save the image + if settings.saveToGallery + && (processedImage.flags.contains(.edited) == true + || processedImage.flags.contains(.gallery) == false) + { + _ = ImageSaver(image: processedImage.image) { error in + var isSaved = false + if error == nil { + isSaved = true + } + self.returnImage(processedImage, isSaved: isSaved) + } + } else { + self.returnImage(processedImage, isSaved: false) + } + } + + fileprivate func showPrompt() { + // Build the action sheet + let alert = UIAlertController( + title: settings.userPromptText.title, message: nil, + preferredStyle: UIAlertController.Style.actionSheet) + alert.addAction( + UIAlertAction( + title: settings.userPromptText.photoAction, style: .default, + handler: { [weak self] (_: UIAlertAction) in + self?.showPhotos() })) - alert.addAction(UIAlertAction(title: settings.userPromptText.cameraAction, style: .default, handler: { [weak self] (_: UIAlertAction) in - self?.showCamera() + alert.addAction( + UIAlertAction( + title: settings.userPromptText.cameraAction, style: .default, + handler: { [weak self] (_: UIAlertAction) in + self?.showCamera() })) - alert.addAction(UIAlertAction(title: settings.userPromptText.cancelAction, style: .cancel, handler: { [weak self] (_: UIAlertAction) in - self?.invoke?.reject("User cancelled photos app prompt") + alert.addAction( + UIAlertAction( + title: settings.userPromptText.cancelAction, style: .cancel, + handler: { [weak self] (_: UIAlertAction) in + self?.invoke?.reject("User cancelled photos app prompt") })) - UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: alert) - self.manager.viewController?.present(alert, animated: true, completion: nil) + UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: alert) + self.manager.viewController?.present(alert, animated: true, completion: nil) + } + + fileprivate func showCamera() { + // check if we have a camera + if manager.isSimEnvironment + || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) + { + Logger.error("[PLUGIN]", "Camera", "-", "Camera not available in simulator") + invoke?.reject("Camera not available while running in Simulator") + return } - - func showCamera() { - // check if we have a camera - if manager.isSimEnvironment || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) { - Logger.error("[PLUGIN]", "Camera", "-", "Camera not available in simulator") - invoke?.reject("Camera not available while running in Simulator") - return - } - // check for permission - let authStatus = AVCaptureDevice.authorizationStatus(for: .video) - if authStatus == .restricted || authStatus == .denied { - invoke?.reject("User denied access to camera") - return - } - // we either already have permission or can prompt - AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in - if granted { - DispatchQueue.main.async { - self?.presentCameraPicker() - } - } else { - self?.invoke?.reject("User denied access to camera") - } - } + // check for permission + let authStatus = AVCaptureDevice.authorizationStatus(for: .video) + if authStatus == .restricted || authStatus == .denied { + invoke?.reject("User denied access to camera") + return } - - func showPhotos() { - // check for permission - let authStatus = PHPhotoLibrary.authorizationStatus() - if authStatus == .restricted || authStatus == .denied { - invoke?.reject("User denied access to photos") - return - } - // we either already have permission or can prompt - if authStatus == .authorized { - presentSystemAppropriateImagePicker() - } else { - PHPhotoLibrary.requestAuthorization({ [weak self] (status) in - if status == PHAuthorizationStatus.authorized { - DispatchQueue.main.async { [weak self] in - self?.presentSystemAppropriateImagePicker() - } - } else { - self?.invoke?.reject("User denied access to photos") - } - }) + // we either already have permission or can prompt + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + if granted { + DispatchQueue.main.async { + self?.presentCameraPicker() } + } else { + self?.invoke?.reject("User denied access to camera") + } } - - func presentCameraPicker() { - let picker = UIImagePickerController() - picker.delegate = self - picker.allowsEditing = self.settings.allowEditing - // select the input - picker.sourceType = .camera - if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { - picker.cameraDevice = .rear - } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) { - picker.cameraDevice = .front - } - // present - picker.modalPresentationStyle = settings.presentationStyle - if settings.presentationStyle == .popover { - picker.popoverPresentationController?.delegate = self - UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) - } - manager.viewController?.present(picker, animated: true, completion: nil) + } + + fileprivate func showPhotos() { + // check for permission + let authStatus = PHPhotoLibrary.authorizationStatus() + if authStatus == .restricted || authStatus == .denied { + invoke?.reject("User denied access to photos") + return } - - func presentSystemAppropriateImagePicker() { - if #available(iOS 14, *) { - presentPhotoPicker() + // we either already have permission or can prompt + if authStatus == .authorized { + presentSystemAppropriateImagePicker() + } else { + PHPhotoLibrary.requestAuthorization({ [weak self] (status) in + if status == PHAuthorizationStatus.authorized { + DispatchQueue.main.async { [weak self] in + self?.presentSystemAppropriateImagePicker() + } } else { - presentImagePicker() + self?.invoke?.reject("User denied access to photos") } + }) } - - func presentImagePicker() { - let picker = UIImagePickerController() - picker.delegate = self - picker.allowsEditing = self.settings.allowEditing - // select the input - picker.sourceType = .photoLibrary - // present - picker.modalPresentationStyle = settings.presentationStyle - if settings.presentationStyle == .popover { - picker.popoverPresentationController?.delegate = self - UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) - } - manager.viewController?.present(picker, animated: true, completion: nil) - } - - @available(iOS 14, *) - func presentPhotoPicker() { - var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) - configuration.selectionLimit = self.multiple ? (self.invoke?.getInt("limit") ?? 0) : 1 - configuration.filter = .images - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - // present - picker.modalPresentationStyle = settings.presentationStyle - if settings.presentationStyle == .popover { - picker.popoverPresentationController?.delegate = self - UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) - } - manager.viewController?.present(picker, animated: true, completion: nil) + } + + fileprivate func presentCameraPicker() { + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = self.settings.allowEditing + // select the input + picker.sourceType = .camera + if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) { + picker.cameraDevice = .rear + } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) + { + picker.cameraDevice = .front } - - func saveTemporaryImage(_ data: Data) throws -> URL { - var url: URL - repeat { - imageCounter += 1 - url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("photo-\(imageCounter).jpg") - } while FileManager.default.fileExists(atPath: url.path) - - try data.write(to: url, options: .atomic) - return url + // present + picker.modalPresentationStyle = settings.presentationStyle + if settings.presentationStyle == .popover { + picker.popoverPresentationController?.delegate = self + UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) } - - func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? { - var selectedImage: UIImage? - var flags: PhotoFlags = [] - // get the image - if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { - selectedImage = edited // use the edited version - flags = flags.union([.edited]) - } else if let original = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - selectedImage = original // use the original version - } - guard let image = selectedImage else { - return nil - } - var metadata: [String: Any] = [:] - // get the image's metadata from the picker or from the photo album - if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { - metadata = photoMetadata - } else { - flags = flags.union([.gallery]) - } - if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { - metadata = asset.imageData - } - // get the result - var result = processedImage(from: image, with: metadata) - result.flags = flags - return result - } - - func processedImage(from image: UIImage, with metadata: [String: Any]?) -> ProcessedImage { - var result = ProcessedImage(image: image, metadata: metadata ?? [:]) - // resizing the image only makes sense if we have real values to which to constrain it - if settings.shouldResize, settings.width > 0 || settings.height > 0 { - result.image = result.image.reformat(to: CGSize(width: settings.width, height: settings.height)) - result.overwriteMetadataOrientation(to: 1) - } else if settings.shouldCorrectOrientation { - // resizing implicitly reformats the image so this is only needed if we aren't resizing - result.image = result.image.reformat() - result.overwriteMetadataOrientation(to: 1) - } - return result + manager.viewController?.present(picker, animated: true, completion: nil) + } + + fileprivate func presentSystemAppropriateImagePicker() { + if #available(iOS 14, *) { + presentPhotoPicker() + } else { + presentImagePicker() + } + } + + fileprivate func presentImagePicker() { + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = self.settings.allowEditing + // select the input + picker.sourceType = .photoLibrary + // present + picker.modalPresentationStyle = settings.presentationStyle + if settings.presentationStyle == .popover { + picker.popoverPresentationController?.delegate = self + UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) + } + manager.viewController?.present(picker, animated: true, completion: nil) + } + + @available(iOS 14, *) + fileprivate func presentPhotoPicker() { + var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) + configuration.selectionLimit = self.multiple ? (self.invoke?.getInt("limit") ?? 0) : 1 + configuration.filter = .images + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + // present + picker.modalPresentationStyle = settings.presentationStyle + if settings.presentationStyle == .popover { + picker.popoverPresentationController?.delegate = self + UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker) + } + manager.viewController?.present(picker, animated: true, completion: nil) + } + + fileprivate func saveTemporaryImage(_ data: Data) throws -> URL { + var url: URL + repeat { + imageCounter += 1 + url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent( + "photo-\(imageCounter).jpg") + } while FileManager.default.fileExists(atPath: url.path) + + try data.write(to: url, options: .atomic) + return url + } + + fileprivate func processImage(from info: [UIImagePickerController.InfoKey: Any]) + -> ProcessedImage? + { + var selectedImage: UIImage? + var flags: PhotoFlags = [] + // get the image + if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + selectedImage = edited // use the edited version + flags = flags.union([.edited]) + } else if let original = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + selectedImage = original // use the original version + } + guard let image = selectedImage else { + return nil + } + var metadata: [String: Any] = [:] + // get the image's metadata from the picker or from the photo album + if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { + metadata = photoMetadata + } else { + flags = flags.union([.gallery]) + } + if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset { + metadata = asset.imageData + } + // get the result + var result = processedImage(from: image, with: metadata) + result.flags = flags + return result + } + + fileprivate func processedImage(from image: UIImage, with metadata: [String: Any]?) + -> ProcessedImage + { + var result = ProcessedImage(image: image, metadata: metadata ?? [:]) + // resizing the image only makes sense if we have real values to which to constrain it + if settings.shouldResize, settings.width > 0 || settings.height > 0 { + result.image = result.image.reformat( + to: CGSize(width: settings.width, height: settings.height)) + result.overwriteMetadataOrientation(to: 1) + } else if settings.shouldCorrectOrientation { + // resizing implicitly reformats the image so this is only needed if we aren't resizing + result.image = result.image.reformat() + result.overwriteMetadataOrientation(to: 1) } + return result + } } @_cdecl("init_plugin_camera") diff --git a/plugins/camera/ios/Sources/CameraTypes.swift b/plugins/camera/ios/Sources/CameraTypes.swift index adfb5640..d3601def 100644 --- a/plugins/camera/ios/Sources/CameraTypes.swift +++ b/plugins/camera/ios/Sources/CameraTypes.swift @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + import UIKit // MARK: - Public diff --git a/plugins/camera/ios/Sources/ImageSaver.swift b/plugins/camera/ios/Sources/ImageSaver.swift index a33b43b0..7d967087 100644 --- a/plugins/camera/ios/Sources/ImageSaver.swift +++ b/plugins/camera/ios/Sources/ImageSaver.swift @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + import UIKit class ImageSaver: NSObject { diff --git a/plugins/camera/ios/Tests/PluginTests/PluginTests.swift b/plugins/camera/ios/Tests/PluginTests/PluginTests.swift index 4f8e9ace..651e1f08 100644 --- a/plugins/camera/ios/Tests/PluginTests/PluginTests.swift +++ b/plugins/camera/ios/Tests/PluginTests/PluginTests.swift @@ -1,3 +1,8 @@ + +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + import XCTest @testable import ExamplePlugin