@capgo/capacitor-file-compressor 8.0.34 → 8.1.0

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.
@@ -1,10 +1,14 @@
1
1
  import Foundation
2
2
  import Capacitor
3
3
  import UIKit
4
+ import ImageIO
5
+ import UniformTypeIdentifiers
4
6
 
5
7
  @objc(FileCompressorPlugin)
6
8
  public class FileCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
7
- private let pluginVersion: String = "8.0.34"
9
+ private let pluginVersion: String = "8.1.0"
10
+ private let minimumQuality: CGFloat = 0.1
11
+ private let qualityStep: CGFloat = 0.05
8
12
  public let identifier = "FileCompressorPlugin"
9
13
  public let jsName = "FileCompressor"
10
14
  public let pluginMethods: [CAPPluginMethod] = [
@@ -23,47 +27,47 @@ public class FileCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
23
27
  let height = call.getInt("height")
24
28
  let mimeType = call.getString("mimeType") ?? "image/jpeg"
25
29
 
26
- // Validate mime type for iOS (only JPEG supported)
27
- if mimeType != "image/jpeg" {
28
- call.reject("Only image/jpeg is supported on iOS")
30
+ guard let outputFormat = ImageOutputFormat(mimeType: mimeType) else {
31
+ call.reject("Unsupported output mimeType: \(mimeType). Supported: \(ImageOutputFormat.supportedMimeTypes.joined(separator: ", "))")
29
32
  return
30
33
  }
31
34
 
32
- // Validate quality range
33
35
  if quality < 0.0 || quality > 1.0 {
34
36
  call.reject("quality must be between 0.0 and 1.0")
35
37
  return
36
38
  }
37
39
 
38
- // Load image from path
39
- guard let image = loadImage(from: path) else {
40
- call.reject("Failed to load image from path")
40
+ guard let sourceURL = resolveFileURL(from: path) else {
41
+ call.reject("Failed to resolve image path")
41
42
  return
42
43
  }
43
44
 
44
- // Resize image if dimensions are provided
45
- var processedImage = image
46
- if let targetWidth = width, let targetHeight = height {
47
- processedImage = resizeImage(image: image, targetWidth: CGFloat(targetWidth), targetHeight: CGFloat(targetHeight))
48
- } else if let targetWidth = width {
49
- let aspectRatio = image.size.height / image.size.width
50
- let targetHeight = CGFloat(targetWidth) * aspectRatio
51
- processedImage = resizeImage(image: image, targetWidth: CGFloat(targetWidth), targetHeight: targetHeight)
52
- } else if let targetHeight = height {
53
- let aspectRatio = image.size.width / image.size.height
54
- let targetWidth = CGFloat(targetHeight) * aspectRatio
55
- processedImage = resizeImage(image: image, targetWidth: targetWidth, targetHeight: CGFloat(targetHeight))
45
+ guard let sourceImage = loadCGImage(from: sourceURL) else {
46
+ call.reject("Failed to load image from path")
47
+ return
56
48
  }
57
49
 
58
- // Compress image to JPEG
59
- guard let imageData = processedImage.jpegData(compressionQuality: CGFloat(quality)) else {
50
+ let targetSize = calculateTargetSize(
51
+ width: CGFloat(sourceImage.width),
52
+ height: CGFloat(sourceImage.height),
53
+ maxWidth: width.map { CGFloat($0) },
54
+ maxHeight: height.map { CGFloat($0) }
55
+ )
56
+ let processedImage = resizeCGImage(sourceImage, targetSize: targetSize)
57
+ let maxBytes = fileSize(at: sourceURL)
58
+
59
+ guard let imageData = encodeImage(
60
+ processedImage,
61
+ format: outputFormat,
62
+ quality: CGFloat(quality),
63
+ maxBytes: maxBytes
64
+ ) else {
60
65
  call.reject("Failed to compress image")
61
66
  return
62
67
  }
63
68
 
64
- // Save compressed image to temporary file
65
69
  let tempDirectory = FileManager.default.temporaryDirectory
66
- let fileName = "compressed_\(UUID().uuidString).jpg"
70
+ let fileName = "compressed_\(UUID().uuidString).\(outputFormat.fileExtension)"
67
71
  let fileURL = tempDirectory.appendingPathComponent(fileName)
68
72
 
69
73
  do {
@@ -76,41 +80,192 @@ public class FileCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
76
80
  }
77
81
  }
78
82
 
79
- private func loadImage(from path: String) -> UIImage? {
80
- // Handle different path types
83
+ private func resolveFileURL(from path: String) -> URL? {
81
84
  if path.hasPrefix("file://") {
82
- let url = URL(string: path)
83
- if let url = url, let data = try? Data(contentsOf: url) {
84
- return UIImage(data: data)
85
- }
86
- } else if path.hasPrefix("/") {
87
- // Absolute file path
88
- if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
89
- return UIImage(data: data)
90
- }
91
- } else if path.hasPrefix("content://") || path.hasPrefix("file:///") {
92
- // Try to parse as URL
93
- if let url = URL(string: path), let data = try? Data(contentsOf: url) {
94
- return UIImage(data: data)
95
- }
85
+ return URL(string: path)
96
86
  }
97
87
 
98
- // Try loading from photo library asset identifier
99
- // This would require Photos framework integration
88
+ if path.hasPrefix("/") {
89
+ return URL(fileURLWithPath: path)
90
+ }
91
+
92
+ if let url = URL(string: path), url.scheme != nil {
93
+ return url
94
+ }
100
95
 
101
96
  return nil
102
97
  }
103
98
 
104
- private func resizeImage(image: UIImage, targetWidth: CGFloat, targetHeight: CGFloat) -> UIImage {
105
- let size = CGSize(width: targetWidth, height: targetHeight)
106
- let renderer = UIGraphicsImageRenderer(size: size)
99
+ private func fileSize(at url: URL) -> Int? {
100
+ guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
101
+ let size = attributes[.size] as? NSNumber else {
102
+ return nil
103
+ }
104
+
105
+ return size.intValue
106
+ }
107
+
108
+ private func loadCGImage(from url: URL) -> CGImage? {
109
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
110
+ return nil
111
+ }
112
+
113
+ return CGImageSourceCreateImageAtIndex(source, 0, nil)
114
+ }
115
+
116
+ private func calculateTargetSize(
117
+ width: CGFloat,
118
+ height: CGFloat,
119
+ maxWidth: CGFloat?,
120
+ maxHeight: CGFloat?
121
+ ) -> CGSize {
122
+ if maxWidth == nil && maxHeight == nil {
123
+ return CGSize(width: width, height: height)
124
+ }
125
+
126
+ var ratio = CGFloat(1)
127
+
128
+ if let maxWidth = maxWidth, let maxHeight = maxHeight {
129
+ ratio = min(maxWidth / width, maxHeight / height, 1)
130
+ } else if let maxWidth = maxWidth {
131
+ ratio = min(maxWidth / width, 1)
132
+ } else if let maxHeight = maxHeight {
133
+ ratio = min(maxHeight / height, 1)
134
+ }
135
+
136
+ return CGSize(width: width * ratio, height: height * ratio)
137
+ }
138
+
139
+ private func resizeCGImage(_ image: CGImage, targetSize: CGSize) -> CGImage {
140
+ let targetWidth = Int(targetSize.width.rounded())
141
+ let targetHeight = Int(targetSize.height.rounded())
142
+
143
+ if targetWidth == image.width && targetHeight == image.height {
144
+ return image
145
+ }
146
+
147
+ guard let context = CGContext(
148
+ data: nil,
149
+ width: targetWidth,
150
+ height: targetHeight,
151
+ bitsPerComponent: 8,
152
+ bytesPerRow: 0,
153
+ space: CGColorSpaceCreateDeviceRGB(),
154
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
155
+ ) else {
156
+ return image
157
+ }
158
+
159
+ context.interpolationQuality = .high
160
+ context.draw(image, in: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight))
161
+
162
+ return context.makeImage() ?? image
163
+ }
164
+
165
+ private func encodeImage(
166
+ _ image: CGImage,
167
+ format: ImageOutputFormat,
168
+ quality: CGFloat,
169
+ maxBytes: Int?
170
+ ) -> Data? {
171
+ var currentQuality = quality
172
+
173
+ while true {
174
+ guard let data = encodeImage(image, format: format, quality: currentQuality) else {
175
+ return nil
176
+ }
177
+
178
+ let withinSizeLimit = maxBytes.map { data.count <= $0 } ?? true
179
+ if withinSizeLimit || currentQuality <= minimumQuality || !format.supportsQuality {
180
+ return data
181
+ }
107
182
 
108
- return renderer.image { _ in
109
- image.draw(in: CGRect(origin: .zero, size: size))
183
+ currentQuality = max(minimumQuality, currentQuality - qualityStep)
110
184
  }
111
185
  }
112
186
 
187
+ private func encodeImage(_ image: CGImage, format: ImageOutputFormat, quality: CGFloat) -> Data? {
188
+ let data = NSMutableData()
189
+ guard let destination = CGImageDestinationCreateWithData(data, format.utType, 1, nil) else {
190
+ return nil
191
+ }
192
+
193
+ var properties: [CFString: Any] = [:]
194
+ if format.supportsQuality {
195
+ properties[kCGImageDestinationLossyCompressionQuality] = quality
196
+ }
197
+
198
+ CGImageDestinationAddImage(destination, image, properties as CFDictionary)
199
+
200
+ guard CGImageDestinationFinalize(destination) else {
201
+ return nil
202
+ }
203
+
204
+ return data as Data
205
+ }
206
+
113
207
  @objc func getPluginVersion(_ call: CAPPluginCall) {
114
208
  call.resolve(["version": self.pluginVersion])
115
209
  }
116
210
  }
211
+
212
+ private struct ImageOutputFormat {
213
+ let mimeType: String
214
+ let utType: CFString
215
+ let fileExtension: String
216
+ let supportsQuality: Bool
217
+
218
+ init(mimeType: String, utType: CFString, fileExtension: String, supportsQuality: Bool) {
219
+ self.mimeType = mimeType
220
+ self.utType = utType
221
+ self.fileExtension = fileExtension
222
+ self.supportsQuality = supportsQuality
223
+ }
224
+
225
+ init?(mimeType: String) {
226
+ switch mimeType.lowercased() {
227
+ case "image/jpeg", "image/jpg":
228
+ self.init(
229
+ mimeType: "image/jpeg",
230
+ utType: UTType.jpeg.identifier as CFString,
231
+ fileExtension: "jpg",
232
+ supportsQuality: true
233
+ )
234
+ case "image/png":
235
+ self.init(
236
+ mimeType: "image/png",
237
+ utType: UTType.png.identifier as CFString,
238
+ fileExtension: "png",
239
+ supportsQuality: false
240
+ )
241
+ case "image/heif", "image/heic":
242
+ self.init(
243
+ mimeType: "image/heif",
244
+ utType: UTType.heic.identifier as CFString,
245
+ fileExtension: "heic",
246
+ supportsQuality: true
247
+ )
248
+ case "image/webp":
249
+ if #available(iOS 14.0, *) {
250
+ self.init(
251
+ mimeType: "image/webp",
252
+ utType: UTType.webP.identifier as CFString,
253
+ fileExtension: "webp",
254
+ supportsQuality: true
255
+ )
256
+ } else {
257
+ return nil
258
+ }
259
+ default:
260
+ return nil
261
+ }
262
+ }
263
+
264
+ static var supportedMimeTypes: [String] {
265
+ if #available(iOS 14.0, *) {
266
+ return ["image/jpeg", "image/png", "image/heif", "image/heic", "image/webp"]
267
+ }
268
+
269
+ return ["image/jpeg", "image/png", "image/heif", "image/heic"]
270
+ }
271
+ }
@@ -2,8 +2,8 @@ import XCTest
2
2
  @testable import FileCompressorPlugin
3
3
 
4
4
  class FileCompressorPluginTests: XCTestCase {
5
- func testEcho() {
6
- // This is an example of a functional test case for a plugin.
7
- // Use XCTAssert and related functions to verify your tests produce the correct results.
8
- }
5
+ func testEcho() {
6
+ // This is an example of a functional test case for a plugin.
7
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
8
+ }
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-file-compressor",
3
- "version": "8.0.34",
3
+ "version": "8.1.0",
4
4
  "description": "Capacitor plugin for efficient image compression supporting PNG, JPEG, and WebP formats across iOS, Android, and Web platforms",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",