TSImageCompress.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. //
  2. // TSImageCompress.swift
  3. // Pods
  4. //
  5. // Created by 100Years on 2025/4/15.
  6. //
  7. import UIKit
  8. // MARK: - UIImage 扩展(Alpha 通道检测和缩放)
  9. public extension UIImage {
  10. /// 检查图片是否有 Alpha 通道(透明度)
  11. func hasAlphaChannel() -> Bool {
  12. guard let cgImage = self.cgImage else { return false }
  13. let alphaInfo = cgImage.alphaInfo
  14. return alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast
  15. }
  16. /// 按比例缩放图片
  17. func resized(withScaleFactor scaleFactor: CGFloat) -> UIImage {
  18. let newSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
  19. let renderer = UIGraphicsImageRenderer(size: newSize)
  20. return renderer.image { _ in
  21. self.draw(in: CGRect(origin: .zero, size: newSize))
  22. }
  23. }
  24. }
  25. public class TSImageCompress {
  26. // MARK: - 核心压缩方法
  27. public static func compressImageToTargetSize(
  28. _ image: UIImage,
  29. targetSizeKB: Int,
  30. preserveTransparency: Bool
  31. ) -> Data? {
  32. let targetBytes = targetSizeKB * 1024
  33. let hasAlpha = image.hasAlphaChannel()
  34. let usePNG = preserveTransparency && hasAlpha
  35. if usePNG {
  36. let compressPNGImage = compressPNGImage(image, targetBytes: targetBytes)
  37. if let compressedData = compressPNGImage {
  38. // saveData(compressedData: compressedData)
  39. print("PNG 压缩后大小: \(compressedData.count / 1024)KB")
  40. }
  41. return compressPNGImage
  42. } else {
  43. let compressJPEGImage = compressJPEGImage(image, targetBytes: targetBytes)
  44. if let compressJPEGImage = compressJPEGImage {
  45. // saveData(compressedData: compressJPEGImage)
  46. print("JPEG 压缩后大小: \(compressJPEGImage.count / 1024)KB")
  47. }
  48. return compressJPEGImage
  49. }
  50. }
  51. // MARK: - PNG 压缩逻辑(调整尺寸)
  52. static private func compressPNGImage(_ image: UIImage, targetBytes: Int) -> Data? {
  53. var currentImage = image
  54. var currentData = currentImage.pngData()
  55. var currentBytes = currentData?.count ?? 0
  56. // 如果已经满足目标大小,直接返回
  57. guard currentBytes > targetBytes else { return currentData }
  58. var scaleFactor: CGFloat = 0.9
  59. let minScaleFactor: CGFloat = 0.1
  60. while scaleFactor >= minScaleFactor {
  61. let scaledImage = currentImage.resized(withScaleFactor: scaleFactor)
  62. guard let newData = scaledImage.pngData() else { break }
  63. let newBytes = newData.count
  64. if newBytes <= targetBytes {
  65. return newData // 达到目标
  66. }
  67. if newBytes >= currentBytes {
  68. break // 无法继续优化
  69. }
  70. currentImage = scaledImage
  71. currentData = newData
  72. currentBytes = newBytes
  73. scaleFactor -= 0.1
  74. }
  75. return currentBytes <= targetBytes ? currentData : nil
  76. }
  77. // MARK: - JPEG 压缩逻辑(先调质量,后调尺寸)
  78. static private func compressJPEGImage(_ image: UIImage, targetBytes: Int) -> Data? {
  79. var compressionQuality: CGFloat = 1.0
  80. var currentImage = image
  81. var currentData = currentImage.jpegData(compressionQuality: compressionQuality)
  82. var currentBytes = currentData?.count ?? 0
  83. // 第一步:降低 JPEG 质量
  84. while currentBytes > targetBytes && compressionQuality > 0.1 {
  85. compressionQuality -= 0.1
  86. currentData = currentImage.jpegData(compressionQuality: compressionQuality)
  87. currentBytes = currentData?.count ?? 0
  88. }
  89. guard currentBytes > targetBytes else { return currentData }
  90. // 第二步:缩小尺寸
  91. var scaleFactor: CGFloat = 0.9
  92. let minScaleFactor: CGFloat = 0.1
  93. while scaleFactor >= minScaleFactor {
  94. let scaledImage = currentImage.resized(withScaleFactor: scaleFactor)
  95. guard let newData = scaledImage.jpegData(compressionQuality: compressionQuality) else { break }
  96. let newBytes = newData.count
  97. if newBytes <= targetBytes {
  98. return newData // 达到目标
  99. }
  100. if newBytes >= currentBytes {
  101. break // 无法继续优化
  102. }
  103. currentImage = scaledImage
  104. currentData = newData
  105. currentBytes = newBytes
  106. scaleFactor -= 0.1
  107. }
  108. return currentBytes <= targetBytes ? currentData : nil
  109. }
  110. }
  111. extension TSImageCompress {
  112. static func saveData(compressedData:Data){
  113. // 写入到沙盒的 "Images" 子目录
  114. let fileName = "compressed_\(Date().timeIntervalSince1970).jpg"
  115. if let fileURL = writeDataToSandbox(data: compressedData, fileName: fileName, subDirectory: "Images") {
  116. print("文件已保存至: \(fileURL.path)")
  117. // 可选:读取文件验证
  118. if let savedData = try? Data(contentsOf: fileURL) {
  119. print("读取文件大小: \(savedData.count / 1024)KB")
  120. }
  121. }
  122. }
  123. static func getDocumentsDirectory() -> URL {
  124. let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
  125. return paths[0] // 返回第一个路径(通常唯一)
  126. }
  127. static func writeDataToSandbox(data: Data, fileName: String, subDirectory: String? = nil) -> URL? {
  128. let baseDirectory: URL
  129. // 1. 确定基础目录(默认 Documents)
  130. baseDirectory = getDocumentsDirectory()
  131. // 2. 拼接子目录(如果存在)
  132. var targetDirectory = baseDirectory
  133. if let subDir = subDirectory {
  134. targetDirectory = baseDirectory.appendingPathComponent(subDir)
  135. }
  136. // 3. 创建子目录(如果不存在)
  137. do {
  138. try FileManager.default.createDirectory(at: targetDirectory, withIntermediateDirectories: true, attributes: nil)
  139. } catch {
  140. print("创建目录失败: \(error.localizedDescription)")
  141. return nil
  142. }
  143. // 4. 拼接最终文件路径
  144. let fileURL = targetDirectory.appendingPathComponent(fileName)
  145. // 5. 写入数据
  146. do {
  147. try data.write(to: fileURL)
  148. // print("文件写入成功: \(fileURL.path)")
  149. return fileURL
  150. } catch {
  151. print("写入文件失败: \(error.localizedDescription)")
  152. return nil
  153. }
  154. }
  155. }