@onekeyfe/react-native-image-crop-picker 3.0.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/ReactNativeImageCropPicker.podspec +40 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +144 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +24 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
  12. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
  13. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
  14. package/ios/ImageCropPickerError.swift +57 -0
  15. package/ios/ImageCropPickerImageProcessor.swift +343 -0
  16. package/ios/ImageCropPickerSession.swift +407 -0
  17. package/ios/ReactNativeImageCropPicker.swift +65 -0
  18. package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
  19. package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
  20. package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
  21. package/ios/TOCropViewController/LICENSE +21 -0
  22. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
  23. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
  24. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
  25. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
  26. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
  27. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
  28. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
  29. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
  30. package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
  31. package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
  32. package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
  33. package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
  34. package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
  35. package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
  36. package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
  37. package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
  38. package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
  39. package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  40. package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
  41. package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
  42. package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
  43. package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
  44. package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
  45. package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
  46. package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
  47. package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
  48. package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
  49. package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  50. package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  51. package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  52. package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
  53. package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
  54. package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
  55. package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  56. package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
  57. package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  58. package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
  59. package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
  60. package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
  61. package/ios/TOCropViewController/TOCropViewController.h +479 -0
  62. package/ios/TOCropViewController/TOCropViewController.m +1355 -0
  63. package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
  64. package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
  65. package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
  66. package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
  67. package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
  68. package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
  69. package/ios/TOCropViewController/Views/TOCropView.h +311 -0
  70. package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
  71. package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
  72. package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
  73. package/lib/module/index.js +83 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/package.json +1 -0
  76. package/lib/typescript/package.json +1 -0
  77. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
  78. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
  79. package/lib/typescript/src/index.d.ts +31 -0
  80. package/lib/typescript/src/index.d.ts.map +1 -0
  81. package/nitro.json +23 -0
  82. package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
  83. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
  84. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
  85. package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
  86. package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
  92. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
  93. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
  94. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
  95. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
  96. package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
  97. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
  98. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
  99. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
  100. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
  101. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
  102. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
  103. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
  104. package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
  105. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  106. package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
  107. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  108. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
  109. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
  110. package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
  111. package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
  112. package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
  113. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
  114. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
  115. package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
  116. package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
  117. package/package.json +173 -0
  118. package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
  119. package/src/index.tsx +134 -0
@@ -0,0 +1,407 @@
1
+ import NitroModules
2
+ import PhotosUI
3
+ import ReactNativeNativeLogger
4
+ import UIKit
5
+ import UniformTypeIdentifiers
6
+
7
+ // One picker or cropper flow. It owns the JS promise until the presented UI
8
+ // reports back, and settles it exactly once.
9
+ final class ImageCropPickerSession: NSObject {
10
+ enum Mode {
11
+ case picker
12
+ case cropper(path: String)
13
+ }
14
+
15
+ private let mode: Mode
16
+ private let config: ImageCropPickerConfig
17
+ private let promise: Promise<PickedImage>
18
+ private let onFinish: (ImageCropPickerSession) -> Void
19
+
20
+ // PHPickerViewController connects to its out-of-process service before it
21
+ // appears, so a requested presentation can stay pending for a moment.
22
+ private static let pendingPresentationTimeout: TimeInterval = 10
23
+
24
+ // Main thread state.
25
+ private var pickerController: PHPickerViewController?
26
+ private var cropController: TOCropViewController?
27
+ private var loadingView: UIView?
28
+ private var pickedFilename: String?
29
+ private var sourceScale: CGFloat = 1
30
+ private var presentationRequestedAt: Date?
31
+ private var hasPresented = false
32
+ private var isBusy = false
33
+ private var isFinished = false
34
+
35
+ init(
36
+ mode: Mode,
37
+ config: ImageCropPickerConfig,
38
+ promise: Promise<PickedImage>,
39
+ onFinish: @escaping (ImageCropPickerSession) -> Void
40
+ ) {
41
+ self.mode = mode
42
+ self.config = config
43
+ self.promise = promise
44
+ self.onFinish = onFinish
45
+ super.init()
46
+ }
47
+
48
+ // Whether the session still has UI on screen or work in flight.
49
+ var isActive: Bool {
50
+ guard !isFinished else {
51
+ return false
52
+ }
53
+ if isBusy {
54
+ return true
55
+ }
56
+ guard let rootController = rootController else {
57
+ return false
58
+ }
59
+ if rootController.presentingViewController != nil || rootController.isBeingPresented {
60
+ return true
61
+ }
62
+ if !hasPresented, let presentationRequestedAt {
63
+ return Date().timeIntervalSince(presentationRequestedAt) < Self.pendingPresentationTimeout
64
+ }
65
+ return false
66
+ }
67
+
68
+ func start() {
69
+ switch mode {
70
+ case .picker:
71
+ presentPicker()
72
+ case .cropper(let path):
73
+ loadCropperImage(path: path)
74
+ }
75
+ }
76
+
77
+ // Settles a session whose UI disappeared without calling back.
78
+ func abandon() {
79
+ finish(.failure(ImageCropPickerError.cancelled))
80
+ }
81
+
82
+ private var rootController: UIViewController? {
83
+ return pickerController ?? cropController
84
+ }
85
+
86
+ private var isPickerMode: Bool {
87
+ if case .picker = mode {
88
+ return true
89
+ }
90
+ return false
91
+ }
92
+
93
+ // MARK: - Picker
94
+
95
+ private func presentPicker() {
96
+ guard let presenter = Self.topViewController() else {
97
+ finish(.failure(ImageCropPickerError.noViewController))
98
+ return
99
+ }
100
+
101
+ // PHPickerViewController runs out of process and needs no photo library
102
+ // permission, so a previously denied permission can't block the picker.
103
+ var configuration = PHPickerConfiguration()
104
+ configuration.filter = .images
105
+ configuration.selectionLimit = 1
106
+ configuration.preferredAssetRepresentationMode = .current
107
+
108
+ let picker = PHPickerViewController(configuration: configuration)
109
+ picker.delegate = self
110
+ picker.modalPresentationStyle = .fullScreen
111
+ pickerController = picker
112
+ present(picker, from: presenter)
113
+ }
114
+
115
+ private func loadPickedImage(_ result: PHPickerResult, in picker: PHPickerViewController) {
116
+ let provider = result.itemProvider
117
+ guard let typeIdentifier = Self.imageTypeIdentifier(of: provider) else {
118
+ dismissAll { self.finish(.failure(ImageCropPickerError.noImageData)) }
119
+ return
120
+ }
121
+
122
+ isBusy = true
123
+ let filename = provider.suggestedName
124
+ pickedFilename = filename
125
+ showLoading(in: picker.view)
126
+
127
+ provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [self] url, _ in
128
+ // The file is removed once this handler returns, so decode it right here.
129
+ if let url, let decoded = ImageCropPickerImageProcessor.decodeImage(at: url) {
130
+ handlePickedImage(decoded, filename: filename)
131
+ return
132
+ }
133
+ provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { [self] data, _ in
134
+ handlePickedImage(
135
+ data.flatMap { ImageCropPickerImageProcessor.decodeImage(data: $0) },
136
+ filename: filename
137
+ )
138
+ }
139
+ }
140
+ }
141
+
142
+ // Runs on the item provider's background queue.
143
+ private func handlePickedImage(_ decoded: DecodedImage?, filename: String?) {
144
+ guard let decoded else {
145
+ DispatchQueue.main.async { [self] in
146
+ isBusy = false
147
+ hideLoading()
148
+ dismissAll { self.finish(.failure(ImageCropPickerError.noImageData)) }
149
+ }
150
+ return
151
+ }
152
+
153
+ guard !config.cropping else {
154
+ DispatchQueue.main.async { [self] in
155
+ isBusy = false
156
+ hideLoading()
157
+ guard !isFinished, let picker = pickerController else {
158
+ return
159
+ }
160
+ sourceScale = decoded.sourceScale
161
+ presentCropper(image: decoded.image, from: picker)
162
+ }
163
+ return
164
+ }
165
+
166
+ let result = Result {
167
+ try ImageCropPickerImageProcessor.makeResult(
168
+ image: decoded.image,
169
+ config: config,
170
+ cropRect: nil,
171
+ filename: filename
172
+ )
173
+ }
174
+ DispatchQueue.main.async { [self] in
175
+ isBusy = false
176
+ hideLoading()
177
+ dismissAll { self.finish(result) }
178
+ }
179
+ }
180
+
181
+ // MARK: - Cropper
182
+
183
+ private func loadCropperImage(path: String) {
184
+ isBusy = true
185
+ DispatchQueue.global(qos: .userInitiated).async { [self] in
186
+ let loaded = Result { try ImageCropPickerImageProcessor.loadImage(fromPath: path) }
187
+ DispatchQueue.main.async { [self] in
188
+ isBusy = false
189
+ switch loaded {
190
+ case .success(let decoded):
191
+ guard let presenter = Self.topViewController() else {
192
+ finish(.failure(ImageCropPickerError.noViewController))
193
+ return
194
+ }
195
+ sourceScale = decoded.sourceScale
196
+ presentCropper(image: decoded.image, from: presenter)
197
+ case .failure(let error):
198
+ finish(.failure(error))
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ private func presentCropper(image: UIImage, from presenter: UIViewController) {
205
+ let controller: TOCropViewController
206
+ if config.cropperCircleOverlay {
207
+ controller = TOCropViewController(croppingStyle: .circular, image: image)
208
+ } else {
209
+ controller = TOCropViewController(image: image)
210
+ if let targetSize = config.targetSize {
211
+ controller.aspectRatioPreset = targetSize
212
+ }
213
+ controller.aspectRatioLockEnabled = !config.freeStyleCropEnabled
214
+ controller.resetAspectRatioEnabled = !controller.aspectRatioLockEnabled
215
+ }
216
+
217
+ controller.title = config.cropperToolbarTitle
218
+ controller.delegate = self
219
+ if let color = ImageCropPickerImageProcessor.color(fromHex: config.cropperChooseColor) {
220
+ controller.doneButtonColor = color
221
+ }
222
+ if let color = ImageCropPickerImageProcessor.color(fromHex: config.cropperCancelColor) {
223
+ controller.cancelButtonColor = color
224
+ }
225
+ controller.doneButtonTitle = config.cropperChooseText
226
+ controller.cancelButtonTitle = config.cropperCancelText
227
+ controller.rotateButtonsHidden = config.cropperRotateButtonsHidden
228
+ controller.modalPresentationStyle = .fullScreen
229
+ controller.modalTransitionStyle = .coverVertical
230
+
231
+ cropController = controller
232
+ present(controller, from: presenter)
233
+ }
234
+
235
+ // MARK: - Completion
236
+
237
+ private func present(_ controller: UIViewController, from presenter: UIViewController) {
238
+ // UIKit silently ignores a presentation from a controller that is off
239
+ // screen or already presenting, and the promise would never settle.
240
+ guard presenter.viewIfLoaded?.window != nil, presenter.presentedViewController == nil else {
241
+ if controller === cropController {
242
+ cropController = nil
243
+ }
244
+ dismissAll { self.finish(.failure(ImageCropPickerError.noViewController)) }
245
+ return
246
+ }
247
+ presentationRequestedAt = Date()
248
+ presenter.present(controller, animated: true) { [weak self] in
249
+ self?.hasPresented = true
250
+ }
251
+ }
252
+
253
+ private func dismissAll(completion: @escaping () -> Void) {
254
+ guard let presenting = rootController?.presentingViewController else {
255
+ completion()
256
+ return
257
+ }
258
+ presenting.dismiss(animated: true, completion: completion)
259
+ }
260
+
261
+ private func finish(_ result: Result<PickedImage, Error>) {
262
+ guard !isFinished else {
263
+ return
264
+ }
265
+ isFinished = true
266
+ hideLoading()
267
+ pickerController = nil
268
+ cropController = nil
269
+
270
+ switch result {
271
+ case .success(let image):
272
+ promise.resolve(withResult: image)
273
+ case .failure(let error):
274
+ if let pickerError = error as? ImageCropPickerError,
275
+ pickerError.code != .pickerCancelled {
276
+ OneKeyLog.warn("ImageCropPicker", "\(pickerError)")
277
+ }
278
+ promise.reject(withError: error)
279
+ }
280
+ onFinish(self)
281
+ }
282
+
283
+ private func showLoading(in view: UIView) {
284
+ hideLoading()
285
+ let overlay = UIView(frame: view.bounds)
286
+ overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
287
+ overlay.backgroundColor = UIColor.black.withAlphaComponent(0.4)
288
+
289
+ let indicator = UIActivityIndicatorView(style: .large)
290
+ indicator.color = .white
291
+ indicator.center = CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY)
292
+ indicator.autoresizingMask = [
293
+ .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin,
294
+ ]
295
+ indicator.startAnimating()
296
+ overlay.addSubview(indicator)
297
+
298
+ view.addSubview(overlay)
299
+ loadingView = overlay
300
+ }
301
+
302
+ private func hideLoading() {
303
+ loadingView?.removeFromSuperview()
304
+ loadingView = nil
305
+ }
306
+
307
+ // MARK: - Helpers
308
+
309
+ private static func imageTypeIdentifier(of provider: NSItemProvider) -> String? {
310
+ let identifier = provider.registeredTypeIdentifiers.first { identifier in
311
+ UTType(identifier)?.conforms(to: .image) ?? false
312
+ }
313
+ if let identifier {
314
+ return identifier
315
+ }
316
+ return provider.hasItemConformingToTypeIdentifier(UTType.image.identifier)
317
+ ? UTType.image.identifier
318
+ : nil
319
+ }
320
+
321
+ static func topViewController() -> UIViewController? {
322
+ let sceneWindows = UIApplication.shared.connectedScenes
323
+ .compactMap { $0 as? UIWindowScene }
324
+ .filter { $0.activationState != .unattached && $0.activationState != .background }
325
+ .flatMap { $0.windows }
326
+ let appDelegateWindow = UIApplication.shared.delegate?.window ?? nil
327
+ let window = sceneWindows.first { $0.isKeyWindow && $0.windowLevel == .normal && $0.rootViewController != nil }
328
+ ?? appDelegateWindow
329
+ ?? sceneWindows.first { $0.rootViewController != nil }
330
+
331
+ var controller = window?.rootViewController
332
+ while let presented = controller?.presentedViewController, !presented.isBeingDismissed {
333
+ controller = presented
334
+ }
335
+ return controller
336
+ }
337
+ }
338
+
339
+ extension ImageCropPickerSession: PHPickerViewControllerDelegate {
340
+ func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
341
+ guard !isFinished, !isBusy, cropController == nil else {
342
+ return
343
+ }
344
+ guard let result = results.first else {
345
+ dismissAll { self.finish(.failure(ImageCropPickerError.cancelled)) }
346
+ return
347
+ }
348
+ loadPickedImage(result, in: picker)
349
+ }
350
+ }
351
+
352
+ extension ImageCropPickerSession: TOCropViewControllerDelegate {
353
+ func cropViewController(
354
+ _ cropViewController: TOCropViewController,
355
+ didCropTo image: UIImage,
356
+ with cropRect: CGRect,
357
+ angle: Int
358
+ ) {
359
+ guard !isFinished, !isBusy else {
360
+ return
361
+ }
362
+ isBusy = true
363
+ showLoading(in: cropViewController.view)
364
+
365
+ let config = self.config
366
+ let filename = pickedFilename
367
+ // Report the crop rect in the coordinates of the original, undownsampled image.
368
+ let sourceCropRect = CGRect(
369
+ x: (cropRect.origin.x * sourceScale).rounded(),
370
+ y: (cropRect.origin.y * sourceScale).rounded(),
371
+ width: (cropRect.width * sourceScale).rounded(),
372
+ height: (cropRect.height * sourceScale).rounded()
373
+ )
374
+
375
+ DispatchQueue.global(qos: .userInitiated).async { [self] in
376
+ let result = Result {
377
+ try ImageCropPickerImageProcessor.makeResult(
378
+ image: ImageCropPickerImageProcessor.resizeCroppedImage(image, config: config),
379
+ config: config,
380
+ cropRect: sourceCropRect,
381
+ filename: filename
382
+ )
383
+ }
384
+ DispatchQueue.main.async { [self] in
385
+ isBusy = false
386
+ hideLoading()
387
+ dismissAll { self.finish(result) }
388
+ }
389
+ }
390
+ }
391
+
392
+ func cropViewController(
393
+ _ cropViewController: TOCropViewController,
394
+ didFinishCancelled cancelled: Bool
395
+ ) {
396
+ guard !isFinished, !isBusy else {
397
+ return
398
+ }
399
+ if isPickerMode, pickerController?.presentingViewController != nil {
400
+ // Go back to the photo picker, like react-native-image-crop-picker.
401
+ cropController = nil
402
+ cropViewController.dismiss(animated: true)
403
+ return
404
+ }
405
+ dismissAll { self.finish(.failure(ImageCropPickerError.cancelled)) }
406
+ }
407
+ }
@@ -0,0 +1,65 @@
1
+ import NitroModules
2
+ import UIKit
3
+
4
+ class ReactNativeImageCropPicker: HybridReactNativeImageCropPickerSpec {
5
+ // Main thread only.
6
+ private var activeSession: ImageCropPickerSession?
7
+
8
+ func openPicker(options: ImageCropPickerOptions) throws -> Promise<PickedImage> {
9
+ return startSession(mode: .picker, config: ImageCropPickerConfig(options))
10
+ }
11
+
12
+ func openCropper(path: String, options: ImageCropPickerOptions) throws -> Promise<PickedImage> {
13
+ return startSession(
14
+ mode: .cropper(path: path),
15
+ config: ImageCropPickerConfig(options, forceCropping: true)
16
+ )
17
+ }
18
+
19
+ func clean() throws -> Promise<Void> {
20
+ return Promise.parallel {
21
+ try ImageCropPickerImageProcessor.cleanTemporaryDirectory()
22
+ }
23
+ }
24
+
25
+ func cleanSingle(path: String) throws -> Promise<Void> {
26
+ return Promise.parallel {
27
+ try ImageCropPickerImageProcessor.removeFile(atPath: path)
28
+ }
29
+ }
30
+
31
+ private func startSession(
32
+ mode: ImageCropPickerSession.Mode,
33
+ config: ImageCropPickerConfig
34
+ ) -> Promise<PickedImage> {
35
+ let promise = Promise<PickedImage>()
36
+ DispatchQueue.main.async {
37
+ self.beginSession(mode: mode, config: config, promise: promise)
38
+ }
39
+ return promise
40
+ }
41
+
42
+ private func beginSession(
43
+ mode: ImageCropPickerSession.Mode,
44
+ config: ImageCropPickerConfig,
45
+ promise: Promise<PickedImage>
46
+ ) {
47
+ if let activeSession {
48
+ if activeSession.isActive {
49
+ promise.reject(withError: ImageCropPickerError.inProgress)
50
+ return
51
+ }
52
+ // Its UI went away without calling back; don't block new requests on it.
53
+ activeSession.abandon()
54
+ }
55
+
56
+ let session = ImageCropPickerSession(mode: mode, config: config, promise: promise) {
57
+ [weak self] finishedSession in
58
+ if self?.activeSession === finishedSession {
59
+ self?.activeSession = nil
60
+ }
61
+ }
62
+ activeSession = session
63
+ session.start()
64
+ }
65
+ }
@@ -0,0 +1,39 @@
1
+ //
2
+ // UIImage+CropRotate.h
3
+ //
4
+ // Copyright 2015-2026 Timothy Oliver. All rights reserved.
5
+ //
6
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ // of this software and associated documentation files (the "Software"), to
8
+ // deal in the Software without restriction, including without limitation the
9
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ // sell copies of the Software, and to permit persons to whom the Software is
11
+ // furnished to do so, subject to the following conditions:
12
+ //
13
+ // The above copyright notice and this permission notice shall be included in
14
+ // all copies or substantial portions of the Software.
15
+ //
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21
+ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ #import <UIKit/UIKit.h>
24
+
25
+ NS_ASSUME_NONNULL_BEGIN
26
+
27
+ @interface UIImage (TOCropRotate)
28
+
29
+ /// Crops a portion of an existing image object and returns it as a new image
30
+ /// @param frame The region inside the image to crop (in the image's point space, ie image.size)
31
+ /// @param angle If any, the angle the image is rotated at as well
32
+ /// @param circular Whether the resulting image is returned as a square or a circle
33
+ - (nonnull UIImage *)croppedImageWithFrame:(CGRect)frame
34
+ angle:(NSInteger)angle
35
+ circularClip:(BOOL)circular;
36
+
37
+ @end
38
+
39
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,94 @@
1
+ //
2
+ // UIImage+CropRotate.m
3
+ //
4
+ // Copyright 2015-2026 Timothy Oliver. All rights reserved.
5
+ //
6
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ // of this software and associated documentation files (the "Software"), to
8
+ // deal in the Software without restriction, including without limitation the
9
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ // sell copies of the Software, and to permit persons to whom the Software is
11
+ // furnished to do so, subject to the following conditions:
12
+ //
13
+ // The above copyright notice and this permission notice shall be included in
14
+ // all copies or substantial portions of the Software.
15
+ //
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21
+ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ #import "UIImage+CropRotate.h"
24
+
25
+ @implementation UIImage (TOCropRotate)
26
+
27
+ - (BOOL)hasAlpha {
28
+ CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(self.CGImage);
29
+ return (alphaInfo == kCGImageAlphaFirst || alphaInfo == kCGImageAlphaLast ||
30
+ alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaPremultipliedLast);
31
+ }
32
+
33
+ - (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circularClip:(BOOL)circular {
34
+ UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat new];
35
+
36
+ #if defined(__IPHONE_17_0)
37
+ // When the source image is HDR, request an HDR-capable rendering format so its
38
+ // highlights aren't silently tone-mapped down to SDR. Falls back to the default
39
+ // format wherever HDR rendering isn't supported.
40
+ if (@available(iOS 17.0, *)) {
41
+ if (self.isHighDynamicRange) {
42
+ UITraitCollection *hdrTraits = [UITraitCollection traitCollectionWithImageDynamicRange:UIImageDynamicRangeHigh];
43
+ UIGraphicsImageRendererFormat *hdrFormat = [UIGraphicsImageRendererFormat formatForTraitCollection:hdrTraits];
44
+ if (hdrFormat.supportsHighDynamicRange) {
45
+ format = hdrFormat;
46
+ }
47
+ }
48
+ }
49
+ #endif
50
+
51
+ format.opaque = !self.hasAlpha && !circular;
52
+ format.scale = self.scale;
53
+
54
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:frame.size format:format];
55
+ UIImage *croppedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
56
+ CGContextRef context = rendererContext.CGContext;
57
+
58
+ // If we're capturing a circular image, set the clip mask first
59
+ if (circular) {
60
+ CGContextAddEllipseInRect(context, (CGRect){CGPointZero, frame.size});
61
+ CGContextClip(context);
62
+ }
63
+
64
+ // Offset the origin (Which is the top left corner) to start where our cropping origin is
65
+ CGContextTranslateCTM(context, -frame.origin.x, -frame.origin.y);
66
+
67
+ // If an angle was supplied, rotate the entire canvas + coordinate space to match
68
+ if (angle != 0) {
69
+ // Rotation in radians
70
+ CGFloat rotation = angle * (M_PI / 180.0f);
71
+
72
+ // Work out the new bounding size of the canvas after rotation
73
+ CGRect imageBounds = (CGRect){CGPointZero, self.size};
74
+ CGRect rotatedBounds = CGRectApplyAffineTransform(imageBounds,
75
+ CGAffineTransformMakeRotation(rotation));
76
+ // As we're rotating from the top left corner, and not the center of the canvas, the frame
77
+ // will have rotated out of our visible canvas. Compensate for this.
78
+ CGContextTranslateCTM(context, -rotatedBounds.origin.x, -rotatedBounds.origin.y);
79
+
80
+ // Perform the rotation transformation
81
+ CGContextRotateCTM(context, rotation);
82
+ }
83
+
84
+ // Draw the image with all of the transformation parameters applied.
85
+ // We do not need to worry about specifying the size here since we're already
86
+ // constrained by the context image size
87
+ [self drawAtPoint:CGPointZero];
88
+ }];
89
+
90
+ // Re-apply the retina scale we originally had
91
+ return [UIImage imageWithCGImage:croppedImage.CGImage scale:self.scale orientation:UIImageOrientationUp];
92
+ }
93
+
94
+ @end
@@ -0,0 +1,63 @@
1
+ //
2
+ // TOCropViewConstants.h
3
+ //
4
+ // Copyright 2015-2026 Timothy Oliver. All rights reserved.
5
+ //
6
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ // of this software and associated documentation files (the "Software"), to
8
+ // deal in the Software without restriction, including without limitation the
9
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ // sell copies of the Software, and to permit persons to whom the Software is
11
+ // furnished to do so, subject to the following conditions:
12
+ //
13
+ // The above copyright notice and this permission notice shall be included in
14
+ // all copies or substantial portions of the Software.
15
+ //
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21
+ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ #import <Foundation/Foundation.h>
24
+
25
+ /**
26
+ The shape of the cropping region of this crop view controller
27
+ */
28
+ typedef NS_ENUM(NSInteger, TOCropViewCroppingStyle) {
29
+ TOCropViewCroppingStyleDefault, // The regular, rectangular crop box
30
+ TOCropViewCroppingStyleCircular // A fixed, circular crop box
31
+ };
32
+
33
+ /**
34
+ Whether the control toolbar is placed at the bottom or the top
35
+ */
36
+ typedef NS_ENUM(NSInteger, TOCropViewControllerToolbarPosition) {
37
+ TOCropViewControllerToolbarPositionBottom, // Bar is placed along the bottom in portrait
38
+ TOCropViewControllerToolbarPositionTop // Bar is placed along the top in portrait (Respects the status bar)
39
+ };
40
+
41
+ static inline NSBundle *TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(NSObject *object) {
42
+ #if SWIFT_PACKAGE
43
+ // SPM is supposed to support the keyword SWIFTPM_MODULE_BUNDLE
44
+ // but I can't figure out how to make it work, so doing it manually
45
+ NSString *bundleName = @"TOCropViewController_TOCropViewController";
46
+ #else
47
+ NSString *bundleName = @"TOCropViewControllerBundle";
48
+ #endif
49
+ NSBundle *resourceBundle = nil;
50
+ NSBundle *classBundle = [NSBundle bundleForClass:object.class];
51
+ NSURL *resourceBundleURL = [classBundle URLForResource:bundleName withExtension:@"bundle"];
52
+ if (resourceBundleURL) {
53
+ resourceBundle = [[NSBundle alloc] initWithURL:resourceBundleURL];
54
+ #ifndef NDEBUG
55
+ if (resourceBundle == nil) {
56
+ @throw [[NSException alloc] initWithName:@"BundleAccessor" reason:[NSString stringWithFormat:@"unable to find bundle named %@", bundleName] userInfo:nil];
57
+ }
58
+ #endif
59
+ } else {
60
+ resourceBundle = classBundle;
61
+ }
62
+ return resourceBundle;
63
+ }
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2025 Tim Oliver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.