capacitor-camera-view 2.4.0 → 3.0.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/CapacitorCameraView.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +341 -49
- package/android/build.gradle +0 -1
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraError.kt +42 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +945 -207
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +87 -43
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +28 -2
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +6 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +15 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/TorchModeState.kt +15 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +1 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +73 -19
- package/dist/docs.json +475 -30
- package/dist/esm/definitions.d.ts +478 -26
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/utils.d.ts +59 -15
- package/dist/esm/utils.js +79 -38
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +191 -13
- package/dist/esm/web.js +624 -141
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +711 -179
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +711 -179
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +147 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +41 -19
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -1
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +78 -23
- package/ios/Sources/CameraViewPlugin/CameraViewManager+DeferredStart.swift +37 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Focus.swift +131 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Lifecycle.swift +156 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +73 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+ResolutionSelection.swift +57 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Rotation.swift +195 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +24 -12
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +22 -73
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Zoom.swift +113 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +450 -403
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +98 -65
- package/ios/Sources/CameraViewPlugin/Utils.swift +61 -7
- package/package.json +25 -9
|
@@ -35,6 +35,7 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
35
35
|
CAPPluginMethod(name: "flipCamera", returnType: CAPPluginReturnPromise),
|
|
36
36
|
CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
|
|
37
37
|
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "setFocusPoint", returnType: CAPPluginReturnPromise),
|
|
38
39
|
CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
|
|
39
40
|
CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
|
|
40
41
|
CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
|
|
@@ -54,25 +55,41 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
54
55
|
public func cameraDidDetectBarcode(_ event: BarcodeDetectedEvent) {
|
|
55
56
|
notifyListeners("barcodeDetected", data: event.toDictionary())
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
public func cameraWasInterrupted(reason: String) {
|
|
60
|
+
notifyListeners("cameraInterrupted", data: ["reason": reason])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public func cameraInterruptionEnded() {
|
|
64
|
+
notifyListeners("cameraResumed", data: [:])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func cameraRuntimeError(message: String, code: Int?) {
|
|
68
|
+
var data: [String: Any] = ["message": message]
|
|
69
|
+
if let code = code {
|
|
70
|
+
data["code"] = code
|
|
71
|
+
}
|
|
72
|
+
notifyListeners("cameraRuntimeError", data: data)
|
|
73
|
+
}
|
|
57
74
|
|
|
58
75
|
@objc func start(_ call: CAPPluginCall) {
|
|
59
76
|
guard let webView = self.webView else {
|
|
60
|
-
call.reject("Cannot find web view")
|
|
77
|
+
call.reject("Cannot find web view", CameraError.missingWebView.code)
|
|
61
78
|
return
|
|
62
79
|
}
|
|
63
|
-
|
|
80
|
+
|
|
64
81
|
maybeRequestCameraAccess { [weak self] granted in
|
|
65
82
|
guard granted else {
|
|
66
|
-
call.reject("Camera access denied")
|
|
83
|
+
call.reject("Camera access denied", CameraError.permissionDenied.code)
|
|
67
84
|
return
|
|
68
85
|
}
|
|
69
|
-
|
|
86
|
+
|
|
70
87
|
self?.implementation.startSession(
|
|
71
88
|
configuration: sessionConfigFromPluginCall(call),
|
|
72
89
|
webView: webView,
|
|
73
90
|
completion: { (error) in
|
|
74
91
|
if let error = error {
|
|
75
|
-
call.reject("Failed to start camera preview",
|
|
92
|
+
call.reject("Failed to start camera preview", error.cameraErrorCode, error)
|
|
76
93
|
return
|
|
77
94
|
}
|
|
78
95
|
call.resolve()
|
|
@@ -97,22 +114,22 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
97
114
|
let saveToFile = call.getBool("saveToFile", false)
|
|
98
115
|
|
|
99
116
|
guard quality >= 0.0 && quality <= 100.0 else {
|
|
100
|
-
call.reject("Quality must be between 0 and 100")
|
|
117
|
+
call.reject("Quality must be between 0 and 100", CameraError.invalidArgument.code)
|
|
101
118
|
return
|
|
102
119
|
}
|
|
103
|
-
|
|
120
|
+
|
|
104
121
|
// Use optimized Data-based capture to avoid double JPEG encoding
|
|
105
122
|
implementation.capturePhotoData(completion: { [weak self] (data, error) in
|
|
106
123
|
if let error = error {
|
|
107
|
-
call.reject("Failed to capture image",
|
|
124
|
+
call.reject("Failed to capture image", error.cameraErrorCode, error)
|
|
108
125
|
return
|
|
109
126
|
}
|
|
110
|
-
|
|
127
|
+
|
|
111
128
|
guard let originalData = data else {
|
|
112
|
-
call.reject("No image data")
|
|
129
|
+
call.reject("No image data", CameraError.captureOutputMissing.code)
|
|
113
130
|
return
|
|
114
131
|
}
|
|
115
|
-
|
|
132
|
+
|
|
116
133
|
// Determine final image data based on quality setting
|
|
117
134
|
// For quality >= 90%, use original camera JPEG data to avoid re-encoding
|
|
118
135
|
// For lower quality, re-encode to reduce file size
|
|
@@ -121,33 +138,34 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
121
138
|
// Use original JPEG data from camera (avoids quality loss and CPU overhead)
|
|
122
139
|
imageData = originalData
|
|
123
140
|
} else {
|
|
124
|
-
// Re-encode at lower quality for smaller file size
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
// Re-encode at lower quality for smaller file size. Uses ImageIO
|
|
142
|
+
// directly (rather than UIImage.jpegData) so EXIF/GPS metadata
|
|
143
|
+
// from the original camera JPEG survives the re-encode.
|
|
144
|
+
guard let compressedData = reencodeJPEG(data: originalData, compressionQuality: quality / 100.0) else {
|
|
145
|
+
call.reject("Failed to compress image", CameraError.imageCompressionFailed.code)
|
|
128
146
|
return
|
|
129
147
|
}
|
|
130
148
|
imageData = compressedData
|
|
131
149
|
}
|
|
132
|
-
|
|
150
|
+
|
|
133
151
|
if saveToFile {
|
|
134
152
|
// Use TempFileManager for tracked temp files with automatic cleanup
|
|
135
153
|
let tempFileURL = TempFileManager.shared.createTempImageFile()
|
|
136
154
|
do {
|
|
137
155
|
try imageData.write(to: tempFileURL)
|
|
138
|
-
|
|
156
|
+
|
|
139
157
|
// Convert file URL to webView-accessible path using Capacitor bridge
|
|
140
158
|
guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
|
|
141
|
-
call.reject("Failed to create web-accessible path")
|
|
159
|
+
call.reject("Failed to create web-accessible path", CameraError.pathConversionFailed.code)
|
|
142
160
|
return
|
|
143
161
|
}
|
|
144
|
-
|
|
162
|
+
|
|
145
163
|
call.resolve([
|
|
146
164
|
"path": tempFileURL.absoluteString,
|
|
147
165
|
"webPath": webPath,
|
|
148
166
|
])
|
|
149
167
|
} catch {
|
|
150
|
-
call.reject("Failed to save image to file",
|
|
168
|
+
call.reject("Failed to save image to file", CameraError.fileWriteFailed.code, error)
|
|
151
169
|
}
|
|
152
170
|
} else {
|
|
153
171
|
// Return as base64
|
|
@@ -163,44 +181,44 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
163
181
|
let saveToFile = call.getBool("saveToFile", false)
|
|
164
182
|
|
|
165
183
|
guard quality >= 0.0 && quality <= 100.0 else {
|
|
166
|
-
call.reject("Quality must be between 0 and 100")
|
|
184
|
+
call.reject("Quality must be between 0 and 100", CameraError.invalidArgument.code)
|
|
167
185
|
return
|
|
168
186
|
}
|
|
169
|
-
|
|
187
|
+
|
|
170
188
|
implementation.captureSnapshot { [weak self] (image, error) in
|
|
171
189
|
if let error = error {
|
|
172
|
-
call.reject("Failed to capture frame",
|
|
190
|
+
call.reject("Failed to capture frame", error.cameraErrorCode, error)
|
|
173
191
|
return
|
|
174
192
|
}
|
|
175
|
-
|
|
193
|
+
|
|
176
194
|
guard let image = image else {
|
|
177
|
-
call.reject("No frame data")
|
|
195
|
+
call.reject("No frame data", CameraError.captureOutputMissing.code)
|
|
178
196
|
return
|
|
179
197
|
}
|
|
180
|
-
|
|
198
|
+
|
|
181
199
|
guard let imageData = image.jpegData(compressionQuality: quality / 100.0) else {
|
|
182
|
-
call.reject("Failed to compress image")
|
|
200
|
+
call.reject("Failed to compress image", CameraError.imageCompressionFailed.code)
|
|
183
201
|
return
|
|
184
202
|
}
|
|
185
|
-
|
|
203
|
+
|
|
186
204
|
if saveToFile {
|
|
187
205
|
// Use TempFileManager for tracked temp files with automatic cleanup
|
|
188
206
|
let tempFileURL = TempFileManager.shared.createTempImageFile()
|
|
189
207
|
do {
|
|
190
208
|
try imageData.write(to: tempFileURL)
|
|
191
|
-
|
|
209
|
+
|
|
192
210
|
// Convert file URL to webView-accessible path using Capacitor bridge
|
|
193
211
|
guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
|
|
194
|
-
call.reject("Failed to create web-accessible path")
|
|
212
|
+
call.reject("Failed to create web-accessible path", CameraError.pathConversionFailed.code)
|
|
195
213
|
return
|
|
196
214
|
}
|
|
197
|
-
|
|
215
|
+
|
|
198
216
|
call.resolve([
|
|
199
217
|
"path": tempFileURL.absoluteString,
|
|
200
218
|
"webPath": webPath,
|
|
201
219
|
])
|
|
202
220
|
} catch {
|
|
203
|
-
call.reject("Failed to save sample to file",
|
|
221
|
+
call.reject("Failed to save sample to file", CameraError.fileWriteFailed.code, error)
|
|
204
222
|
}
|
|
205
223
|
} else {
|
|
206
224
|
// Return as base64
|
|
@@ -216,14 +234,14 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
216
234
|
let videoQuality = call.getString("videoQuality") ?? "highest"
|
|
217
235
|
|
|
218
236
|
guard let parsedVideoQuality = VideoRecordingQuality(rawValue: videoQuality) else {
|
|
219
|
-
call.reject("Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest")
|
|
237
|
+
call.reject("Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest", CameraError.invalidArgument.code)
|
|
220
238
|
return
|
|
221
239
|
}
|
|
222
|
-
|
|
240
|
+
|
|
223
241
|
if enableAudio {
|
|
224
242
|
maybeRequestMicrophoneAccess { [weak self] granted in
|
|
225
243
|
guard granted else {
|
|
226
|
-
call.reject("Microphone access denied")
|
|
244
|
+
call.reject("Microphone access denied", CameraError.permissionDenied.code)
|
|
227
245
|
return
|
|
228
246
|
}
|
|
229
247
|
self?.doStartRecording(call: call, enableAudio: true, videoQuality: parsedVideoQuality)
|
|
@@ -232,7 +250,7 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
232
250
|
doStartRecording(call: call, enableAudio: false, videoQuality: parsedVideoQuality)
|
|
233
251
|
}
|
|
234
252
|
}
|
|
235
|
-
|
|
253
|
+
|
|
236
254
|
private func doStartRecording(
|
|
237
255
|
call: CAPPluginCall,
|
|
238
256
|
enableAudio: Bool,
|
|
@@ -240,30 +258,30 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
240
258
|
) {
|
|
241
259
|
implementation.startRecording(enableAudio: enableAudio, videoQuality: videoQuality) { error in
|
|
242
260
|
if let error = error {
|
|
243
|
-
call.reject("Failed to start recording",
|
|
261
|
+
call.reject("Failed to start recording", error.cameraErrorCode, error)
|
|
244
262
|
return
|
|
245
263
|
}
|
|
246
264
|
call.resolve()
|
|
247
265
|
}
|
|
248
266
|
}
|
|
249
|
-
|
|
267
|
+
|
|
250
268
|
@objc func stopRecording(_ call: CAPPluginCall) {
|
|
251
269
|
implementation.stopRecording { [weak self] (outputURL, error) in
|
|
252
270
|
if let error = error {
|
|
253
|
-
call.reject("Failed to stop recording",
|
|
271
|
+
call.reject("Failed to stop recording", error.cameraErrorCode, error)
|
|
254
272
|
return
|
|
255
273
|
}
|
|
256
|
-
|
|
274
|
+
|
|
257
275
|
guard let outputURL = outputURL else {
|
|
258
|
-
call.reject("No output file URL")
|
|
276
|
+
call.reject("No output file URL", CameraError.captureOutputMissing.code)
|
|
259
277
|
return
|
|
260
278
|
}
|
|
261
|
-
|
|
279
|
+
|
|
262
280
|
guard let webPath = self?.bridge?.portablePath(fromLocalURL: outputURL)?.absoluteString else {
|
|
263
|
-
call.reject("Failed to create web-accessible path")
|
|
281
|
+
call.reject("Failed to create web-accessible path", CameraError.pathConversionFailed.code)
|
|
264
282
|
return
|
|
265
283
|
}
|
|
266
|
-
|
|
284
|
+
|
|
267
285
|
call.resolve([
|
|
268
286
|
"path": outputURL.absoluteString,
|
|
269
287
|
"webPath": webPath,
|
|
@@ -290,12 +308,12 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
@objc func flipCamera(_ call: CAPPluginCall) {
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
implementation.flipCamera { error in
|
|
312
|
+
if let error = error {
|
|
313
|
+
call.reject("Failed to switch camera", error.cameraErrorCode, error)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
295
316
|
call.resolve()
|
|
296
|
-
} catch {
|
|
297
|
-
call.reject("Failed to switch camera", nil, error)
|
|
298
|
-
return
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
|
|
@@ -311,21 +329,36 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
311
329
|
|
|
312
330
|
@objc func setZoom(_ call: CAPPluginCall) {
|
|
313
331
|
guard let level = call.getDouble("level") else {
|
|
314
|
-
call.reject("Zoom level must be provided")
|
|
332
|
+
call.reject("Zoom level must be provided", CameraError.invalidArgument.code)
|
|
315
333
|
return
|
|
316
334
|
}
|
|
317
|
-
|
|
335
|
+
|
|
318
336
|
let ramp = call.getBool("ramp") ?? false
|
|
319
|
-
|
|
337
|
+
|
|
320
338
|
do {
|
|
321
339
|
try implementation.setZoomFactor(level, ramp: ramp)
|
|
322
340
|
call.resolve()
|
|
323
341
|
} catch {
|
|
324
|
-
call.reject("Failed to set zoom level",
|
|
342
|
+
call.reject("Failed to set zoom level", error.cameraErrorCode, error)
|
|
325
343
|
return
|
|
326
344
|
}
|
|
327
345
|
}
|
|
328
346
|
|
|
347
|
+
@objc func setFocusPoint(_ call: CAPPluginCall) {
|
|
348
|
+
guard let pointX = call.getDouble("x"), let pointY = call.getDouble("y") else {
|
|
349
|
+
call.reject("Focus point x and y must be provided", CameraError.invalidArgument.code)
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
implementation.setFocusPoint(x: CGFloat(pointX), y: CGFloat(pointY)) { error in
|
|
354
|
+
if let error = error {
|
|
355
|
+
call.reject("Failed to set focus point", error.cameraErrorCode, error)
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
call.resolve()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
@objc func getFlashMode(_ call: CAPPluginCall) {
|
|
330
363
|
let flashMode = implementation.getFlashMode()
|
|
331
364
|
|
|
@@ -336,7 +369,7 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
336
369
|
|
|
337
370
|
@objc func getSupportedFlashModes(_ call: CAPPluginCall) {
|
|
338
371
|
let supportedFlashModes = implementation.getSupportedFlashModes()
|
|
339
|
-
let supportedFlashModeStrArr = supportedFlashModes.
|
|
372
|
+
let supportedFlashModeStrArr = supportedFlashModes.compactMap { flashModeToStrMap[$0] }
|
|
340
373
|
|
|
341
374
|
call.resolve([
|
|
342
375
|
"flashModes": supportedFlashModeStrArr
|
|
@@ -345,20 +378,20 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
345
378
|
|
|
346
379
|
@objc func setFlashMode(_ call: CAPPluginCall) {
|
|
347
380
|
guard let mode = call.getString("mode") else {
|
|
348
|
-
call.reject("Flash mode must be provided")
|
|
381
|
+
call.reject("Flash mode must be provided", CameraError.invalidArgument.code)
|
|
349
382
|
return
|
|
350
383
|
}
|
|
351
|
-
|
|
384
|
+
|
|
352
385
|
guard let flashMode = strToFlashModeMap[mode] else {
|
|
353
|
-
call.reject("Invalid flash mode")
|
|
386
|
+
call.reject("Invalid flash mode", CameraError.invalidArgument.code)
|
|
354
387
|
return
|
|
355
388
|
}
|
|
356
|
-
|
|
389
|
+
|
|
357
390
|
do {
|
|
358
391
|
try implementation.setFlashMode(flashMode)
|
|
359
392
|
call.resolve()
|
|
360
393
|
} catch {
|
|
361
|
-
call.reject("Failed to set flash mode",
|
|
394
|
+
call.reject("Failed to set flash mode", error.cameraErrorCode, error)
|
|
362
395
|
}
|
|
363
396
|
}
|
|
364
397
|
|
|
@@ -379,22 +412,22 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
379
412
|
|
|
380
413
|
@objc func setTorchMode(_ call: CAPPluginCall) {
|
|
381
414
|
guard let enabled = call.getBool("enabled") else {
|
|
382
|
-
call.reject("Enabled parameter is required")
|
|
415
|
+
call.reject("Enabled parameter is required", CameraError.invalidArgument.code)
|
|
383
416
|
return
|
|
384
417
|
}
|
|
385
|
-
|
|
418
|
+
|
|
386
419
|
let level = call.getFloat("level") ?? 1.0
|
|
387
|
-
|
|
420
|
+
|
|
388
421
|
guard level >= 0.0 && level <= 1.0 else {
|
|
389
|
-
call.reject("Level must be between 0.0 and 1.0")
|
|
422
|
+
call.reject("Level must be between 0.0 and 1.0", CameraError.invalidArgument.code)
|
|
390
423
|
return
|
|
391
424
|
}
|
|
392
|
-
|
|
425
|
+
|
|
393
426
|
do {
|
|
394
427
|
try implementation.setTorchMode(enabled: enabled, level: level)
|
|
395
428
|
call.resolve()
|
|
396
429
|
} catch {
|
|
397
|
-
call.reject("Failed to set torch mode",
|
|
430
|
+
call.reject("Failed to set torch mode", error.cameraErrorCode, error)
|
|
398
431
|
}
|
|
399
432
|
}
|
|
400
433
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
|
+
import ImageIO
|
|
3
|
+
import os
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
|
|
6
|
+
/// Shared logger for camera plugin diagnostics (visible in Console.app / `log stream`),
|
|
7
|
+
/// used instead of `print()` so failures are visible in production logs.
|
|
8
|
+
internal let cameraViewLogger = Logger(subsystem: "com.michaelwolz.capacitorcameraview", category: "CameraViewManager")
|
|
2
9
|
|
|
3
10
|
// MARK: - Camera Type Conversion
|
|
4
11
|
|
|
@@ -57,25 +64,66 @@ public func convertToNativeCameraTypes(_ stringTypes: [String]) -> [AVCaptureDev
|
|
|
57
64
|
return stringTypes.compactMap { convertToNativeCameraType($0) }
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// MARK: - Image Re-encoding
|
|
68
|
+
|
|
69
|
+
/// Re-encodes JPEG data at a lower compression quality while preserving the
|
|
70
|
+
/// original image metadata (EXIF, TIFF, GPS, orientation, etc.).
|
|
71
|
+
///
|
|
72
|
+
/// Uses ImageIO directly instead of round-tripping through `UIImage`/
|
|
73
|
+
/// `UIImage.jpegData(compressionQuality:)`, which silently drops all metadata
|
|
74
|
+
/// from the source JPEG.
|
|
75
|
+
///
|
|
76
|
+
/// - Parameters:
|
|
77
|
+
/// - data: The source JPEG data.
|
|
78
|
+
/// - compressionQuality: The lossy compression quality (0.0-1.0).
|
|
79
|
+
/// - Returns: The re-encoded JPEG data, or nil if the source data or
|
|
80
|
+
/// destination could not be created.
|
|
81
|
+
public func reencodeJPEG(data: Data, compressionQuality: CGFloat) -> Data? {
|
|
82
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let destinationData = NSMutableData()
|
|
87
|
+
guard let destination = CGImageDestinationCreateWithData(
|
|
88
|
+
destinationData,
|
|
89
|
+
UTType.jpeg.identifier as CFString,
|
|
90
|
+
1,
|
|
91
|
+
nil
|
|
92
|
+
) else {
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Start from the source image's own metadata so EXIF/GPS/orientation
|
|
97
|
+
// survive the re-encode, then layer the compression quality on top.
|
|
98
|
+
var properties = (CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]) ?? [:]
|
|
99
|
+
properties[kCGImageDestinationLossyCompressionQuality] = compressionQuality
|
|
100
|
+
|
|
101
|
+
CGImageDestinationAddImageFromSource(destination, source, 0, properties as CFDictionary)
|
|
102
|
+
|
|
103
|
+
guard CGImageDestinationFinalize(destination) else {
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return destinationData as Data
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
// MARK: - Barcode Type Conversion
|
|
70
111
|
|
|
71
112
|
/// All supported barcode types for detection.
|
|
72
113
|
/// These are enabled by default when no specific types are configured.
|
|
114
|
+
///
|
|
115
|
+
/// Note: there is no UPC-A entry. AVFoundation has no UPC-A metadata object
|
|
116
|
+
/// type and reports UPC-A codes as `.ean13` (a UPC-A value is an EAN-13 with a
|
|
117
|
+
/// leading `0`), so on iOS the `upcA` union member is neither requestable nor
|
|
118
|
+
/// emitted — such codes arrive as `ean13`. `.codabar` is available since iOS
|
|
119
|
+
/// 15.4; the deployment target is 16, so no availability guard is needed.
|
|
73
120
|
public let ALL_SUPPORTED_BARCODE_TYPES: [AVMetadataObject.ObjectType] = [
|
|
74
121
|
.qr,
|
|
75
122
|
.code128,
|
|
76
123
|
.code39,
|
|
77
124
|
.code39Mod43,
|
|
78
125
|
.code93,
|
|
126
|
+
.codabar,
|
|
79
127
|
.ean8,
|
|
80
128
|
.ean13,
|
|
81
129
|
.interleaved2of5,
|
|
@@ -101,6 +149,10 @@ public func convertToNativeBarcodeType(_ stringType: String) -> AVMetadataObject
|
|
|
101
149
|
return .code39Mod43
|
|
102
150
|
case "code93":
|
|
103
151
|
return .code93
|
|
152
|
+
case "codabar":
|
|
153
|
+
return .codabar
|
|
154
|
+
// Note: "upcA" is intentionally unmapped. AVFoundation has no UPC-A metadata
|
|
155
|
+
// type and reports UPC-A codes as .ean13, so it cannot be requested on iOS.
|
|
104
156
|
case "ean8":
|
|
105
157
|
return .ean8
|
|
106
158
|
case "ean13":
|
|
@@ -137,6 +189,8 @@ public func convertToStringBarcodeType(_ barcodeType: AVMetadataObject.ObjectTyp
|
|
|
137
189
|
return "code39Mod43"
|
|
138
190
|
case .code93:
|
|
139
191
|
return "code93"
|
|
192
|
+
case .codabar:
|
|
193
|
+
return "codabar"
|
|
140
194
|
case .ean8:
|
|
141
195
|
return "ean8"
|
|
142
196
|
case .ean13:
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-camera-view",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A Capacitor plugin for embedding a live camera feed directly into your app.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/esm/index.d.ts",
|
|
8
8
|
"unpkg": "dist/plugin.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/plugin.cjs.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
9
18
|
"files": [
|
|
10
19
|
"android/src/main/",
|
|
11
20
|
"android/build.gradle",
|
|
@@ -29,23 +38,29 @@
|
|
|
29
38
|
"plugin",
|
|
30
39
|
"native"
|
|
31
40
|
],
|
|
41
|
+
"packageManager": "pnpm@11.18.0",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24",
|
|
44
|
+
"pnpm": ">=11"
|
|
45
|
+
},
|
|
32
46
|
"scripts": {
|
|
33
|
-
"verify": "
|
|
47
|
+
"verify": "pnpm run verify:ios && pnpm run verify:android && pnpm run verify:web",
|
|
34
48
|
"verify:ios": "xcodebuild -scheme CapacitorCameraView -destination generic/platform=iOS",
|
|
35
49
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
36
|
-
"verify:web": "
|
|
37
|
-
"lint": "
|
|
38
|
-
"lint:ios": "
|
|
39
|
-
"fmt": "
|
|
50
|
+
"verify:web": "pnpm run build",
|
|
51
|
+
"lint": "pnpm run eslint",
|
|
52
|
+
"lint:ios": "pnpm run swiftlint lint",
|
|
53
|
+
"fmt": "pnpm run eslint --fix && pnpm run prettier --write && pnpm run swiftlint --fix --format",
|
|
40
54
|
"eslint": "eslint . --ext ts",
|
|
41
55
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
42
56
|
"prettier:check": "prettier --check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
43
57
|
"swiftlint": "node-swiftlint",
|
|
58
|
+
"test": "vitest run",
|
|
44
59
|
"docgen": "docgen --api CameraViewPlugin --output-readme README.md --output-json dist/docs.json",
|
|
45
|
-
"build": "
|
|
60
|
+
"build": "pnpm run clean && pnpm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
46
61
|
"clean": "rimraf ./dist",
|
|
47
62
|
"watch": "tsc --watch",
|
|
48
|
-
"prepublishOnly": "
|
|
63
|
+
"prepublishOnly": "pnpm run build"
|
|
49
64
|
},
|
|
50
65
|
"devDependencies": {
|
|
51
66
|
"@capacitor/android": "^8.3.1",
|
|
@@ -76,7 +91,8 @@
|
|
|
76
91
|
"rollup": "^4.60.2",
|
|
77
92
|
"semantic-release": "^25.0.3",
|
|
78
93
|
"swiftlint": "^2.0.0",
|
|
79
|
-
"typescript": "
|
|
94
|
+
"typescript": "~6.0.3",
|
|
95
|
+
"vitest": "^4.1.10"
|
|
80
96
|
},
|
|
81
97
|
"peerDependencies": {
|
|
82
98
|
"@capacitor/core": ">=8.0.0"
|