@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,57 @@
1
+ import Foundation
2
+
3
+ enum ImageCropPickerErrorCode: String {
4
+ case pickerCancelled = "E_PICKER_CANCELLED"
5
+ case pickerInProgress = "E_PICKER_IN_PROGRESS"
6
+ case failedToShowPicker = "E_FAILED_TO_SHOW_PICKER"
7
+ case noImageDataFound = "E_NO_IMAGE_DATA_FOUND"
8
+ case cropperImageNotFound = "E_CROPPER_IMAGE_NOT_FOUND"
9
+ case cannotSaveImage = "E_CANNOT_SAVE_IMAGE"
10
+ case cleanupError = "E_ERROR_WHILE_CLEANING_FILES"
11
+ }
12
+
13
+ // The JS wrapper parses the "<code>: <message>" description back into
14
+ // `error.code` / `error.message`, matching react-native-image-crop-picker.
15
+ struct ImageCropPickerError: Error, CustomStringConvertible {
16
+ let code: ImageCropPickerErrorCode
17
+ let message: String
18
+
19
+ var description: String {
20
+ return "\(code.rawValue): \(message)"
21
+ }
22
+
23
+ static let cancelled = ImageCropPickerError(
24
+ code: .pickerCancelled,
25
+ message: "User cancelled image selection"
26
+ )
27
+
28
+ static let inProgress = ImageCropPickerError(
29
+ code: .pickerInProgress,
30
+ message: "Another image picker or cropper is already open"
31
+ )
32
+
33
+ static let noViewController = ImageCropPickerError(
34
+ code: .failedToShowPicker,
35
+ message: "Cannot find a view controller to present from"
36
+ )
37
+
38
+ static let noImageData = ImageCropPickerError(
39
+ code: .noImageDataFound,
40
+ message: "Cannot find image data"
41
+ )
42
+
43
+ static let cropperImageNotFound = ImageCropPickerError(
44
+ code: .cropperImageNotFound,
45
+ message: "Can't find the image at the specified path"
46
+ )
47
+
48
+ static let cannotSaveImage = ImageCropPickerError(
49
+ code: .cannotSaveImage,
50
+ message: "Cannot save image. Unable to write to tmp location."
51
+ )
52
+
53
+ static let cleanupFailed = ImageCropPickerError(
54
+ code: .cleanupError,
55
+ message: "Error while cleaning up tmp files"
56
+ )
57
+ }
@@ -0,0 +1,343 @@
1
+ import Foundation
2
+ import ImageIO
3
+ import UIKit
4
+
5
+ // Plain Swift copy of the options, so no C++ backed struct outlives the JS call.
6
+ struct ImageCropPickerConfig {
7
+ let width: Double?
8
+ let height: Double?
9
+ let cropping: Bool
10
+ let includeBase64: Bool
11
+ let compressImageQuality: Double?
12
+ let compressImageMaxWidth: Double?
13
+ let compressImageMaxHeight: Double?
14
+ let freeStyleCropEnabled: Bool
15
+ let cropperCircleOverlay: Bool
16
+ let cropperToolbarTitle: String?
17
+ let cropperChooseText: String?
18
+ let cropperCancelText: String?
19
+ let cropperChooseColor: String?
20
+ let cropperCancelColor: String?
21
+ let cropperRotateButtonsHidden: Bool
22
+
23
+ init(_ options: ImageCropPickerOptions, forceCropping: Bool = false) {
24
+ width = options.width
25
+ height = options.height
26
+ cropping = forceCropping || (options.cropping ?? false)
27
+ includeBase64 = options.includeBase64 ?? false
28
+ compressImageQuality = options.compressImageQuality
29
+ compressImageMaxWidth = options.compressImageMaxWidth
30
+ compressImageMaxHeight = options.compressImageMaxHeight
31
+ freeStyleCropEnabled = options.freeStyleCropEnabled ?? false
32
+ cropperCircleOverlay = options.cropperCircleOverlay ?? false
33
+ cropperToolbarTitle = options.cropperToolbarTitle
34
+ cropperChooseText = options.cropperChooseText
35
+ cropperCancelText = options.cropperCancelText
36
+ cropperChooseColor = options.cropperChooseColor
37
+ cropperCancelColor = options.cropperCancelColor
38
+ cropperRotateButtonsHidden = options.cropperRotateButtonsHidden ?? false
39
+ }
40
+
41
+ var targetSize: CGSize? {
42
+ guard let width, let height, width > 0, height > 0 else {
43
+ return nil
44
+ }
45
+ return CGSize(width: width, height: height)
46
+ }
47
+ }
48
+
49
+ struct DecodedImage {
50
+ let image: UIImage
51
+ // Source pixels per decoded pixel. Greater than 1 when the source was downsampled.
52
+ let sourceScale: CGFloat
53
+ }
54
+
55
+ enum ImageCropPickerImageProcessor {
56
+ // Bounds memory for huge photos (48 MP decodes to ~200 MB). Crop targets are
57
+ // far smaller than this, so no visible quality is lost.
58
+ static let maxDecodedPixelSize = 4096
59
+ static let defaultCompressQuality = 0.8
60
+ static let temporaryDirectoryName = "react-native-image-crop-picker"
61
+
62
+ static func decodeImage(at url: URL) -> DecodedImage? {
63
+ let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
64
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
65
+ return nil
66
+ }
67
+ return decodeImage(source: source)
68
+ }
69
+
70
+ static func decodeImage(data: Data) -> DecodedImage? {
71
+ let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
72
+ guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions) else {
73
+ return nil
74
+ }
75
+ return decodeImage(source: source)
76
+ }
77
+
78
+ private static func decodeImage(source: CGImageSource) -> DecodedImage? {
79
+ guard CGImageSourceGetCount(source) > 0 else {
80
+ return nil
81
+ }
82
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
83
+ let pixelWidth = (properties?[kCGImagePropertyPixelWidth] as? NSNumber)?.intValue ?? 0
84
+ let pixelHeight = (properties?[kCGImagePropertyPixelHeight] as? NSNumber)?.intValue ?? 0
85
+ let sourceMaxDimension = max(pixelWidth, pixelHeight)
86
+ let maxPixelSize = sourceMaxDimension > 0
87
+ ? min(sourceMaxDimension, maxDecodedPixelSize)
88
+ : maxDecodedPixelSize
89
+
90
+ // The thumbnail API applies the EXIF orientation, so the result is always `.up`.
91
+ let thumbnailOptions: [CFString: Any] = [
92
+ kCGImageSourceCreateThumbnailFromImageAlways: true,
93
+ kCGImageSourceCreateThumbnailWithTransform: true,
94
+ kCGImageSourceShouldCacheImmediately: true,
95
+ kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
96
+ ]
97
+ guard let cgImage = CGImageSourceCreateThumbnailAtIndex(
98
+ source,
99
+ 0,
100
+ thumbnailOptions as CFDictionary
101
+ ) else {
102
+ return nil
103
+ }
104
+
105
+ let decodedMaxDimension = max(cgImage.width, cgImage.height)
106
+ let sourceScale = sourceMaxDimension > 0 && decodedMaxDimension > 0
107
+ ? CGFloat(sourceMaxDimension) / CGFloat(decodedMaxDimension)
108
+ : 1
109
+ return DecodedImage(
110
+ image: UIImage(cgImage: cgImage, scale: 1, orientation: .up),
111
+ sourceScale: max(sourceScale, 1)
112
+ )
113
+ }
114
+
115
+ static func loadImage(fromPath path: String) throws -> DecodedImage {
116
+ if path.hasPrefix("data:") {
117
+ guard
118
+ let commaIndex = path.firstIndex(of: ","),
119
+ let data = Data(
120
+ base64Encoded: String(path[path.index(after: commaIndex)...]),
121
+ options: .ignoreUnknownCharacters
122
+ ),
123
+ let decoded = decodeImage(data: data)
124
+ else {
125
+ throw ImageCropPickerError.cropperImageNotFound
126
+ }
127
+ return decoded
128
+ }
129
+
130
+ if let url = URL(string: path), let scheme = url.scheme?.lowercased(),
131
+ scheme == "http" || scheme == "https" {
132
+ guard let data = try? downloadData(from: url), let decoded = decodeImage(data: data) else {
133
+ throw ImageCropPickerError.cropperImageNotFound
134
+ }
135
+ return decoded
136
+ }
137
+
138
+ guard let fileURL = fileURL(fromPath: path),
139
+ let decoded = decodeImage(at: fileURL) else {
140
+ throw ImageCropPickerError.cropperImageNotFound
141
+ }
142
+ return decoded
143
+ }
144
+
145
+ static func fileURL(fromPath path: String) -> URL? {
146
+ if path.hasPrefix("file://") {
147
+ if let url = URL(string: path), url.isFileURL {
148
+ return url
149
+ }
150
+ // Unescaped paths (e.g. containing spaces) are not valid URL strings.
151
+ let rawPath = String(path.dropFirst("file://".count))
152
+ return URL(fileURLWithPath: rawPath.removingPercentEncoding ?? rawPath)
153
+ }
154
+ if path.hasPrefix("/") {
155
+ return URL(fileURLWithPath: path)
156
+ }
157
+ return nil
158
+ }
159
+
160
+ private final class DownloadResult: @unchecked Sendable {
161
+ var data: Data?
162
+ }
163
+
164
+ // Called from a background queue only.
165
+ private static func downloadData(from url: URL) throws -> Data {
166
+ let semaphore = DispatchSemaphore(value: 0)
167
+ let result = DownloadResult()
168
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
169
+ defer { semaphore.signal() }
170
+ guard error == nil else {
171
+ return
172
+ }
173
+ if let httpResponse = response as? HTTPURLResponse,
174
+ !(200..<300).contains(httpResponse.statusCode) {
175
+ return
176
+ }
177
+ result.data = data
178
+ }
179
+ task.resume()
180
+ semaphore.wait()
181
+ guard let data = result.data else {
182
+ throw ImageCropPickerError.cropperImageNotFound
183
+ }
184
+ return data
185
+ }
186
+
187
+ // Scales a cropped image to the requested size; smaller crops are scaled up.
188
+ // With a locked aspect ratio the crop box can still end up a few pixels off
189
+ // that ratio, so the result is scaled to exactly the requested size.
190
+ // Free-style crops keep their own aspect ratio and fit inside it.
191
+ static func resizeCroppedImage(_ image: UIImage, config: ImageCropPickerConfig) -> UIImage {
192
+ guard let targetSize = config.targetSize else {
193
+ return image
194
+ }
195
+ let exactSize = CGSize(width: targetSize.width.rounded(), height: targetSize.height.rounded())
196
+ guard config.freeStyleCropEnabled else {
197
+ return draw(image, size: exactSize)
198
+ }
199
+ let sourceSize = pixelSize(of: image)
200
+ guard sourceSize.width > 0, sourceSize.height > 0 else {
201
+ return image
202
+ }
203
+ let widthRatio = exactSize.width / sourceSize.width
204
+ let heightRatio = exactSize.height / sourceSize.height
205
+ let destinationSize = widthRatio < heightRatio
206
+ ? CGSize(width: exactSize.width, height: (sourceSize.height * widthRatio).rounded())
207
+ : CGSize(width: (sourceSize.width * heightRatio).rounded(), height: exactSize.height)
208
+ return draw(image, size: destinationSize)
209
+ }
210
+
211
+ static func makeResult(
212
+ image: UIImage,
213
+ config: ImageCropPickerConfig,
214
+ cropRect: CGRect?,
215
+ filename: String?
216
+ ) throws -> PickedImage {
217
+ var output = image
218
+ let size = pixelSize(of: output)
219
+ let maxWidth = config.compressImageMaxWidth.map { CGFloat($0) }
220
+ let maxHeight = config.compressImageMaxHeight.map { CGFloat($0) }
221
+ let shouldResizeWidth = maxWidth.map { $0 < size.width } ?? false
222
+ let shouldResizeHeight = maxHeight.map { $0 < size.height } ?? false
223
+ if shouldResizeWidth || shouldResizeHeight {
224
+ let scale = min(
225
+ (maxWidth ?? size.width) / size.width,
226
+ (maxHeight ?? size.height) / size.height
227
+ )
228
+ output = draw(
229
+ output,
230
+ size: CGSize(width: floor(size.width * scale), height: floor(size.height * scale))
231
+ )
232
+ }
233
+
234
+ let quality = min(max(config.compressImageQuality ?? defaultCompressQuality, 0), 1)
235
+ guard let data = output.jpegData(compressionQuality: CGFloat(quality)) else {
236
+ throw ImageCropPickerError.cannotSaveImage
237
+ }
238
+
239
+ let fileURL = try temporaryDirectory()
240
+ .appendingPathComponent(UUID().uuidString)
241
+ .appendingPathExtension("jpg")
242
+ do {
243
+ try data.write(to: fileURL, options: .atomic)
244
+ } catch {
245
+ throw ImageCropPickerError.cannotSaveImage
246
+ }
247
+
248
+ let outputSize = pixelSize(of: output)
249
+ return PickedImage(
250
+ path: fileURL.absoluteString,
251
+ size: Double(data.count),
252
+ width: Double(outputSize.width),
253
+ height: Double(outputSize.height),
254
+ mime: "image/jpeg",
255
+ data: config.includeBase64 ? data.base64EncodedString() : nil,
256
+ cropRect: cropRect.map {
257
+ CropRect(
258
+ x: Double($0.origin.x),
259
+ y: Double($0.origin.y),
260
+ width: Double($0.width),
261
+ height: Double($0.height)
262
+ )
263
+ },
264
+ filename: filename
265
+ )
266
+ }
267
+
268
+ static func temporaryDirectory() throws -> URL {
269
+ let directory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
270
+ .appendingPathComponent(temporaryDirectoryName, isDirectory: true)
271
+ do {
272
+ try FileManager.default.createDirectory(
273
+ at: directory,
274
+ withIntermediateDirectories: true
275
+ )
276
+ } catch {
277
+ throw ImageCropPickerError.cannotSaveImage
278
+ }
279
+ return directory
280
+ }
281
+
282
+ static func cleanTemporaryDirectory() throws {
283
+ let directory = try temporaryDirectory()
284
+ let fileManager = FileManager.default
285
+ do {
286
+ for item in try fileManager.contentsOfDirectory(
287
+ at: directory,
288
+ includingPropertiesForKeys: nil
289
+ ) {
290
+ try fileManager.removeItem(at: item)
291
+ }
292
+ } catch {
293
+ throw ImageCropPickerError.cleanupFailed
294
+ }
295
+ }
296
+
297
+ static func removeFile(atPath path: String) throws {
298
+ guard let fileURL = fileURL(fromPath: path) else {
299
+ throw ImageCropPickerError.cleanupFailed
300
+ }
301
+ do {
302
+ try FileManager.default.removeItem(at: fileURL)
303
+ } catch {
304
+ throw ImageCropPickerError.cleanupFailed
305
+ }
306
+ }
307
+
308
+ static func color(fromHex hex: String?) -> UIColor? {
309
+ guard var value = hex?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
310
+ return nil
311
+ }
312
+ if value.hasPrefix("#") {
313
+ value.removeFirst()
314
+ }
315
+ guard value.count == 6 || value.count == 8, let rgba = UInt64(value, radix: 16) else {
316
+ return nil
317
+ }
318
+ let hasAlpha = value.count == 8
319
+ let red = CGFloat((rgba >> (hasAlpha ? 24 : 16)) & 0xff) / 255
320
+ let green = CGFloat((rgba >> (hasAlpha ? 16 : 8)) & 0xff) / 255
321
+ let blue = CGFloat((rgba >> (hasAlpha ? 8 : 0)) & 0xff) / 255
322
+ let alpha = hasAlpha ? CGFloat(rgba & 0xff) / 255 : 1
323
+ return UIColor(red: red, green: green, blue: blue, alpha: alpha)
324
+ }
325
+
326
+ private static func pixelSize(of image: UIImage) -> CGSize {
327
+ return CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
328
+ }
329
+
330
+ private static func draw(_ image: UIImage, size: CGSize) -> UIImage {
331
+ guard size.width >= 1, size.height >= 1, size != pixelSize(of: image) else {
332
+ return image
333
+ }
334
+ let format = UIGraphicsImageRendererFormat()
335
+ format.scale = 1
336
+ format.opaque = false
337
+ format.preferredRange = .standard
338
+ let renderer = UIGraphicsImageRenderer(size: size, format: format)
339
+ return renderer.image { _ in
340
+ image.draw(in: CGRect(origin: .zero, size: size))
341
+ }
342
+ }
343
+ }