@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.
- package/README.md +44 -18
- package/android/src/main/java/io/capgo/filecompressor/FileCompressorPlugin.java +115 -45
- package/dist/docs.json +21 -13
- package/dist/esm/definitions.d.ts +40 -20
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +5 -0
- package/dist/esm/web.js +50 -23
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +50 -23
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +50 -23
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/FileCompressorPlugin/FileCompressorPlugin.swift +202 -47
- package/ios/Tests/FileCompressorPluginTests/FileCompressorPluginTests.swift +4 -4
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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).
|
|
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
|
|
80
|
-
// Handle different path types
|
|
83
|
+
private func resolveFileURL(from path: String) -> URL? {
|
|
81
84
|
if path.hasPrefix("file://") {
|
|
82
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
105
|
-
let
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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",
|