ClassifyPhotoPlugin.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import Flutter
  2. import Photos
  3. import UIKit
  4. public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
  5. var photoClassifier = ClassifyPhoto()
  6. public static func register(with registrar: FlutterPluginRegistrar) {
  7. let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger())
  8. let instance = ClassifyPhotoPlugin()
  9. registrar.addMethodCallDelegate(instance, channel: channel)
  10. }
  11. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
  12. print("iOS: Received method call: \(call.method)")
  13. switch call.method {
  14. case "getPhoto":
  15. self.getPhoto(flutterResult: result)
  16. case "getStorageInfo":
  17. getStorageInfo(result: result)
  18. case "getPlatformVersion":
  19. result("iOS " + UIDevice.current.systemVersion)
  20. default:
  21. result(FlutterMethodNotImplemented)
  22. }
  23. }
  24. private class func blankof<T>(type:T.Type) -> T {
  25. let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.size)
  26. let val = ptr.pointee
  27. return val
  28. }
  29. /// 磁盘总大小
  30. private class func getTotalDiskSize() -> Int64 {
  31. var fs = blankof(type: statfs.self)
  32. if statfs("/var",&fs) >= 0{
  33. return Int64(UInt64(fs.f_bsize) * fs.f_blocks)
  34. }
  35. return -1
  36. }
  37. private func getStorageInfo(result: @escaping FlutterResult) {
  38. DispatchQueue.global(qos: .userInitiated).async {
  39. var storageInfo: [String: Int64] = [:]
  40. // 获取总容量和可用容量
  41. if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
  42. let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize()
  43. let freeSpace = space[.systemFreeSize] as? Int64 ?? 0
  44. storageInfo["totalSpace"] = totalSpace
  45. storageInfo["freeSpace"] = freeSpace
  46. storageInfo["usedSpace"] = totalSpace - freeSpace
  47. }
  48. // 获取照片占用的空间
  49. let options = PHFetchOptions()
  50. let allPhotos = PHAsset.fetchAssets(with: .image, options: options)
  51. var photoSize: Int64 = 0
  52. let group = DispatchGroup()
  53. let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent)
  54. let semaphore = DispatchSemaphore(value: 10) // 限制并发
  55. allPhotos.enumerateObjects { (asset, index, stop) in
  56. group.enter()
  57. semaphore.wait()
  58. let resources = PHAssetResource.assetResources(for: asset)
  59. if let resource = resources.first {
  60. queue.async {
  61. let options = PHAssetResourceRequestOptions()
  62. options.isNetworkAccessAllowed = true
  63. PHAssetResourceManager.default().requestData(
  64. for: resource,
  65. options: options,
  66. dataReceivedHandler: { data in
  67. photoSize += Int64(data.count)
  68. },
  69. completionHandler: { error in
  70. if let error = error {
  71. print("Error getting photo size: \(error)")
  72. }
  73. semaphore.signal()
  74. group.leave()
  75. }
  76. )
  77. }
  78. } else {
  79. semaphore.signal()
  80. group.leave()
  81. }
  82. }
  83. group.notify(queue: .main) {
  84. storageInfo["photoSpace"] = photoSize
  85. result(storageInfo)
  86. }
  87. }
  88. }
  89. private func getPhoto(flutterResult: @escaping FlutterResult) {
  90. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  91. guard let self = self else { return }
  92. let fetchOptions = PHFetchOptions()
  93. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  94. photoClassifier.classifyPhotos(
  95. assets: allPhotos,
  96. progressHandler: { (stage, progress) in
  97. print("Progress: \(stage) - \(progress)")
  98. },
  99. completion: { result in
  100. var resultData: [[String: Any]] = []
  101. let mainGroup = DispatchGroup()
  102. // 处理截图
  103. mainGroup.enter()
  104. self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in
  105. if !groupData.isEmpty {
  106. resultData.append(["group": groupData, "type": "screenshots"])
  107. }
  108. mainGroup.leave()
  109. }
  110. // 处理相似照片组
  111. for photoGroup in result.similarPhotos {
  112. mainGroup.enter()
  113. self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in
  114. if !groupData.isEmpty {
  115. resultData.append(["group": groupData, "type": "similar"])
  116. }
  117. mainGroup.leave()
  118. }
  119. }
  120. // 处理地点分组
  121. for (location, assets) in result.locations {
  122. mainGroup.enter()
  123. self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in
  124. if !groupData.isEmpty {
  125. resultData.append(["group": groupData, "type": "location", "name": location])
  126. }
  127. mainGroup.leave()
  128. }
  129. }
  130. // 处理人物分组
  131. for (person, assets) in result.people {
  132. mainGroup.enter()
  133. self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in
  134. if !groupData.isEmpty {
  135. resultData.append(["group": groupData, "type": "people"])
  136. }
  137. mainGroup.leave()
  138. }
  139. }
  140. // 处理模糊照片
  141. mainGroup.enter()
  142. self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in
  143. if !groupData.isEmpty {
  144. resultData.append(["group": groupData, "type": "blurry"])
  145. }
  146. mainGroup.leave()
  147. }
  148. mainGroup.notify(queue: .main) {
  149. print("Final result count: \(resultData.count)")
  150. flutterResult(resultData)
  151. }
  152. }
  153. )
  154. }
  155. }
  156. // 处理照片组的辅助方法
  157. private func processPhotoGroup(
  158. assets: [PHAsset],
  159. groupName: String,
  160. sizeInfo: ClassifyPhoto.PhotoSizeInfo,
  161. completion: @escaping ([String: Any]) -> Void
  162. ) {
  163. let photoProcessGroup = DispatchGroup()
  164. var photosData: [[String: Any]] = []
  165. for asset in assets {
  166. photoProcessGroup.enter()
  167. let options = PHContentEditingInputRequestOptions()
  168. options.isNetworkAccessAllowed = true
  169. asset.requestContentEditingInput(with: options) { (input, info) in
  170. defer { photoProcessGroup.leave() }
  171. if let input = input, let url = input.fullSizeImageURL {
  172. let photoInfo: [String: Any] = [
  173. "path": url.path,
  174. "id": asset.localIdentifier,
  175. "width": asset.pixelWidth,
  176. "height": asset.pixelHeight,
  177. "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0
  178. ]
  179. photosData.append(photoInfo)
  180. }
  181. }
  182. }
  183. photoProcessGroup.notify(queue: .main) {
  184. completion([
  185. "photos": photosData,
  186. "totalSize": sizeInfo.totalSize,
  187. "count": sizeInfo.count
  188. ])
  189. }
  190. }
  191. }