@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/ReactNativeImageCropPicker.podspec +40 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +144 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
- package/ios/ImageCropPickerError.swift +57 -0
- package/ios/ImageCropPickerImageProcessor.swift +343 -0
- package/ios/ImageCropPickerSession.swift +407 -0
- package/ios/ReactNativeImageCropPicker.swift +65 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
- package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
- package/ios/TOCropViewController/LICENSE +21 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
- package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
- package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/TOCropViewController.h +479 -0
- package/ios/TOCropViewController/TOCropViewController.m +1355 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
- package/ios/TOCropViewController/Views/TOCropView.h +311 -0
- package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
- package/lib/module/index.js +83 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +31 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
- package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
- package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
- package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
- package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
- package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
- package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
- package/package.json +173 -0
- package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
- 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
|
+
}
|