ClassifyPhotoPlugin.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import Flutter
  2. import StoreKit
  3. import Photos
  4. import UIKit
  5. public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
  6. var photoClassifier = ClassifyPhoto()
  7. public static func register(with registrar: FlutterPluginRegistrar) {
  8. let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger())
  9. let instance = ClassifyPhotoPlugin()
  10. registrar.addMethodCallDelegate(instance, channel: channel)
  11. }
  12. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
  13. print("iOS: Received method call: \(call.method)")
  14. switch call.method {
  15. case "getPhoto":
  16. self.getPhoto(flutterResult: result)
  17. case "getStorageInfo":
  18. getStorageInfo(result: result)
  19. case "getExifInfo":
  20. guard let args = call.arguments as? [String: Any],
  21. let filePath = args["filePath"] as? String else {
  22. result(FlutterError(
  23. code: "INVALID_ARGUMENTS",
  24. message: "Missing filePath parameter",
  25. details: nil
  26. ))
  27. return
  28. }
  29. getExifInfo(filePath: filePath, completion: result)
  30. case "getPhotosSize":
  31. guard let args = call.arguments as? [String: Any],
  32. let assetIds = args["assetIds"] as? [String] else {
  33. result(FlutterError(
  34. code: "INVALID_ARGUMENTS",
  35. message: "Missing filePath parameter",
  36. details: nil
  37. ))
  38. return
  39. }
  40. calculatePhotosSize(assetIds: assetIds, completion: result)
  41. case "getPlatformVersion":
  42. result("iOS " + UIDevice.current.systemVersion)
  43. default:
  44. result(FlutterMethodNotImplemented)
  45. }
  46. }
  47. private class func blankof<T>(type:T.Type) -> T {
  48. let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.size)
  49. let val = ptr.pointee
  50. return val
  51. }
  52. /// 磁盘总大小
  53. private class func getTotalDiskSize() -> Int64 {
  54. var fs = blankof(type: statfs.self)
  55. if statfs("/var",&fs) >= 0{
  56. return Int64(UInt64(fs.f_bsize) * fs.f_blocks)
  57. }
  58. return -1
  59. }
  60. private func getStorageInfo(result: @escaping FlutterResult) {
  61. DispatchQueue.global(qos: .userInitiated).async {
  62. var storageInfo: [String: Int64] = [:]
  63. // 获取总容量和可用容量
  64. if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
  65. let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize()
  66. let freeSpace = space[.systemFreeSize] as? Int64 ?? 0
  67. storageInfo["totalSpace"] = totalSpace
  68. storageInfo["freeSpace"] = freeSpace
  69. storageInfo["usedSpace"] = totalSpace - freeSpace
  70. }
  71. // 获取照片占用的空间
  72. let options = PHFetchOptions()
  73. let allPhotos = PHAsset.fetchAssets(with: .image, options: options)
  74. var photoSize: Int64 = 0
  75. let group = DispatchGroup()
  76. let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent)
  77. let semaphore = DispatchSemaphore(value: 10) // 限制并发
  78. allPhotos.enumerateObjects { (asset, index, stop) in
  79. group.enter()
  80. semaphore.wait()
  81. let resources = PHAssetResource.assetResources(for: asset)
  82. if let resource = resources.first {
  83. queue.async {
  84. let options = PHAssetResourceRequestOptions()
  85. options.isNetworkAccessAllowed = true
  86. PHAssetResourceManager.default().requestData(
  87. for: resource,
  88. options: options,
  89. dataReceivedHandler: { data in
  90. photoSize += Int64(data.count)
  91. },
  92. completionHandler: { error in
  93. if let error = error {
  94. print("Error getting photo size: \(error)")
  95. }
  96. semaphore.signal()
  97. group.leave()
  98. }
  99. )
  100. }
  101. } else {
  102. semaphore.signal()
  103. group.leave()
  104. }
  105. }
  106. group.notify(queue: .main) {
  107. storageInfo["photoSpace"] = photoSize
  108. result(storageInfo)
  109. }
  110. }
  111. }
  112. private func getPhoto(flutterResult: @escaping FlutterResult) {
  113. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  114. guard let self = self else { return }
  115. let fetchOptions = PHFetchOptions()
  116. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  117. photoClassifier.classifyPhotos(
  118. assets: allPhotos,
  119. progressHandler: { (stage, progress) in
  120. print("Progress: \(stage) - \(progress)")
  121. },
  122. completion: { result in
  123. var resultData: [[String: Any]] = []
  124. let mainGroup = DispatchGroup()
  125. // 处理截图
  126. mainGroup.enter()
  127. self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in
  128. if !groupData.isEmpty {
  129. resultData.append(["group": groupData, "type": "screenshots"])
  130. }
  131. mainGroup.leave()
  132. }
  133. // 处理相似照片组
  134. for photoGroup in result.similarPhotos {
  135. mainGroup.enter()
  136. self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in
  137. if !groupData.isEmpty {
  138. resultData.append(["group": groupData, "type": "similar"])
  139. }
  140. mainGroup.leave()
  141. }
  142. }
  143. // 处理地点分组
  144. for (location, assets) in result.locations {
  145. mainGroup.enter()
  146. self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in
  147. if !groupData.isEmpty {
  148. resultData.append(["group": groupData, "type": "location", "name": location])
  149. }
  150. mainGroup.leave()
  151. }
  152. }
  153. // 处理人物分组
  154. for (person, assets) in result.people {
  155. mainGroup.enter()
  156. self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in
  157. if !groupData.isEmpty {
  158. resultData.append(["group": groupData, "type": "people"])
  159. }
  160. mainGroup.leave()
  161. }
  162. }
  163. // 处理模糊照片
  164. mainGroup.enter()
  165. self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in
  166. if !groupData.isEmpty {
  167. resultData.append(["group": groupData, "type": "blurry"])
  168. }
  169. mainGroup.leave()
  170. }
  171. mainGroup.notify(queue: .main) {
  172. print("Final result count: \(resultData.count)")
  173. flutterResult(resultData)
  174. }
  175. }
  176. )
  177. }
  178. }
  179. // 处理照片组的辅助方法
  180. private func processPhotoGroup(
  181. assets: [PHAsset],
  182. groupName: String,
  183. sizeInfo: ClassifyPhoto.PhotoSizeInfo,
  184. completion: @escaping ([String: Any]) -> Void
  185. ) {
  186. let photoProcessGroup = DispatchGroup()
  187. var photosData: [[String: Any]] = []
  188. for asset in assets {
  189. photoProcessGroup.enter()
  190. let options = PHContentEditingInputRequestOptions()
  191. options.isNetworkAccessAllowed = true
  192. asset.requestContentEditingInput(with: options) { (input, info) in
  193. defer { photoProcessGroup.leave() }
  194. if let input = input, let url = input.fullSizeImageURL {
  195. let photoInfo: [String: Any] = [
  196. "path": url.path,
  197. "id": asset.localIdentifier,
  198. "width": asset.pixelWidth,
  199. "height": asset.pixelHeight,
  200. "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0
  201. ]
  202. photosData.append(photoInfo)
  203. }
  204. }
  205. }
  206. photoProcessGroup.notify(queue: .main) {
  207. completion([
  208. "photos": photosData,
  209. "totalSize": sizeInfo.totalSize,
  210. "count": sizeInfo.count
  211. ])
  212. }
  213. }
  214. private func getExifInfo(filePath: String, completion: @escaping FlutterResult) {
  215. // 创建文件 URL
  216. let fileURL: URL
  217. if filePath.starts(with: "file://") {
  218. guard let url = URL(string: filePath) else {
  219. print("Invalid URL string: \(filePath)")
  220. completion([:])
  221. return
  222. }
  223. fileURL = url
  224. } else {
  225. fileURL = URL(fileURLWithPath: filePath)
  226. }
  227. // 检查文件是否存在
  228. guard FileManager.default.fileExists(atPath: fileURL.path) else {
  229. print("File does not exist at path: \(fileURL.path)")
  230. completion([:])
  231. return
  232. }
  233. // 创建图片源
  234. guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
  235. print("Failed to create image source for path: \(fileURL.path)")
  236. completion([:])
  237. return
  238. }
  239. var exifInfo: [String: Any] = [:]
  240. // 获取所有元数据
  241. if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] {
  242. // EXIF 数据
  243. if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] {
  244. exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String]
  245. exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String]
  246. exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String]
  247. exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String]
  248. exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String]
  249. }
  250. // TIFF 数据
  251. if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
  252. exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String]
  253. exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String]
  254. }
  255. // GPS 数据
  256. if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
  257. exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String]
  258. exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String]
  259. exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String]
  260. }
  261. // 图片基本信息
  262. exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String]
  263. exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String]
  264. exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String]
  265. exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String]
  266. exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String]
  267. }
  268. completion(exifInfo)
  269. }
  270. }
  271. // 计算选择的图片大小
  272. extension ClassifyPhotoPlugin {
  273. private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  274. let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  275. var phAssets: [PHAsset] = []
  276. var sizes: Int64 = 0 // 用于存储文件大小
  277. assets.enumerateObjects { (phAsset, _, _) in
  278. phAssets.append(phAsset)
  279. // 获取 PHAsset 的资源
  280. let resources = PHAssetResource.assetResources(for: phAsset)
  281. if let resource = resources.first {
  282. // 获取文件大小
  283. if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong {
  284. sizes += Int64(unsignedInt64)
  285. }
  286. }
  287. }
  288. // 返回文件大小到 Flutter
  289. completion(sizes)
  290. }
  291. }
  292. class SubscriptionHandler: NSObject {
  293. @available(iOS 15.0.0, *)
  294. func checkTrialEligibility() async -> Bool {
  295. do {
  296. // 获取产品信息
  297. let productIds = ["clean.vip.1week"]
  298. let products = try await Product.products(for: Set(productIds))
  299. // 检查第一个产品的试用资格
  300. if let product = products.first {
  301. return await product.subscription?.isEligibleForIntroOffer ?? false
  302. }
  303. return false
  304. } catch {
  305. print("Error checking trial eligibility: \(error)")
  306. return false
  307. }
  308. }
  309. }