@momo-kits/camerakit 0.150.2-beta.3 → 0.150.2-beta.30
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/android/src/main/java/com/rncamerakit/CKCamera.kt +86 -5
- package/dist/specs/CameraNativeComponent.d.ts +0 -1
- package/ios/ReactNativeCameraKit/CameraManager.swift +1 -1
- package/ios/ReactNativeCameraKit/CameraView.swift +78 -11
- package/ios/ReactNativeCameraKit/RNCameraKitModule.mm +1 -1
- package/package.json +6 -34
|
@@ -5,6 +5,7 @@ import android.animation.Animator
|
|
|
5
5
|
import android.animation.AnimatorListenerAdapter
|
|
6
6
|
import android.annotation.SuppressLint
|
|
7
7
|
import android.app.Activity
|
|
8
|
+
import android.content.ContentValues
|
|
8
9
|
import android.content.Context
|
|
9
10
|
import android.content.pm.PackageManager
|
|
10
11
|
import android.graphics.Bitmap
|
|
@@ -14,7 +15,6 @@ import android.hardware.SensorManager
|
|
|
14
15
|
import android.media.AudioManager
|
|
15
16
|
import android.media.MediaActionSound
|
|
16
17
|
import android.net.Uri
|
|
17
|
-
import android.util.DisplayMetrics
|
|
18
18
|
import android.util.Log
|
|
19
19
|
import android.view.*
|
|
20
20
|
import android.widget.FrameLayout
|
|
@@ -30,9 +30,7 @@ import androidx.core.content.ContextCompat
|
|
|
30
30
|
import androidx.lifecycle.LifecycleObserver
|
|
31
31
|
import com.facebook.react.bridge.Arguments
|
|
32
32
|
import com.facebook.react.bridge.Promise
|
|
33
|
-
import com.facebook.react.bridge.WritableMap
|
|
34
33
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
35
|
-
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
36
34
|
import com.rncamerakit.barcode.BarcodeFrame
|
|
37
35
|
import java.io.File
|
|
38
36
|
import java.util.*
|
|
@@ -42,9 +40,13 @@ import kotlin.math.abs
|
|
|
42
40
|
import kotlin.math.max
|
|
43
41
|
import kotlin.math.min
|
|
44
42
|
import android.graphics.Canvas
|
|
43
|
+
import android.graphics.Matrix
|
|
45
44
|
import android.graphics.Paint
|
|
46
45
|
import android.graphics.Rect
|
|
47
46
|
import android.graphics.RectF
|
|
47
|
+
import androidx.exifinterface.media.ExifInterface
|
|
48
|
+
import android.os.Build
|
|
49
|
+
import android.provider.MediaStore
|
|
48
50
|
import android.util.Base64.decode
|
|
49
51
|
import android.util.Size
|
|
50
52
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
@@ -60,6 +62,13 @@ import kotlinx.coroutines.SupervisorJob
|
|
|
60
62
|
import kotlinx.coroutines.launch
|
|
61
63
|
import jp.co.cyberagent.android.gpuimage.GPUImage
|
|
62
64
|
import jp.co.cyberagent.android.gpuimage.filter.GPUImageSharpenFilter
|
|
65
|
+
import java.io.FileOutputStream
|
|
66
|
+
|
|
67
|
+
fun Bitmap.rotate(degrees: Int): Bitmap {
|
|
68
|
+
val matrix = Matrix()
|
|
69
|
+
matrix.postRotate(degrees.toFloat())
|
|
70
|
+
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
class RectOverlay constructor(context: Context) :
|
|
65
74
|
View(context) {
|
|
@@ -502,6 +511,36 @@ open class CKCamera(context: ThemedReactContext) : FrameLayout(context), Lifecyc
|
|
|
502
511
|
|
|
503
512
|
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
504
513
|
try {
|
|
514
|
+
val capturedBitmap = BitmapFactory.decodeFile(output.savedUri!!.path)
|
|
515
|
+
val targetWidth = options["width"] as? Double
|
|
516
|
+
val targetHeight = options["height"] as? Double
|
|
517
|
+
val targetQuality = options["quality"] as? Int ?: 100
|
|
518
|
+
if (targetWidth != null && targetHeight != null) {
|
|
519
|
+
val exif = output.savedUri!!.path?.let { ExifInterface(it) }
|
|
520
|
+
val rotatedBitmap = when (exif?.getAttributeInt(
|
|
521
|
+
ExifInterface.TAG_ORIENTATION,
|
|
522
|
+
ExifInterface.ORIENTATION_UNDEFINED
|
|
523
|
+
)) {
|
|
524
|
+
ExifInterface.ORIENTATION_ROTATE_90 -> capturedBitmap.rotate(90)
|
|
525
|
+
ExifInterface.ORIENTATION_ROTATE_180 -> capturedBitmap.rotate(180)
|
|
526
|
+
ExifInterface.ORIENTATION_ROTATE_270 -> capturedBitmap.rotate(270)
|
|
527
|
+
else -> capturedBitmap
|
|
528
|
+
}
|
|
529
|
+
val resizedBitmap = Bitmap.createScaledBitmap(
|
|
530
|
+
rotatedBitmap,
|
|
531
|
+
targetWidth.toInt(),
|
|
532
|
+
targetHeight.toInt(),
|
|
533
|
+
true
|
|
534
|
+
)
|
|
535
|
+
val outputStream = FileOutputStream(output.savedUri!!.path)
|
|
536
|
+
resizedBitmap.compress(
|
|
537
|
+
Bitmap.CompressFormat.JPEG,
|
|
538
|
+
targetQuality,
|
|
539
|
+
outputStream
|
|
540
|
+
)
|
|
541
|
+
outputStream.close()
|
|
542
|
+
}
|
|
543
|
+
|
|
505
544
|
val uri = output.savedUri ?: Uri.fromFile(outputFile)
|
|
506
545
|
val id = uri?.path
|
|
507
546
|
val name = uri?.lastPathSegment
|
|
@@ -509,14 +548,20 @@ open class CKCamera(context: ThemedReactContext) : FrameLayout(context), Lifecyc
|
|
|
509
548
|
|
|
510
549
|
val savedUri = (output.savedUri ?: outputPath).toString()
|
|
511
550
|
onPictureTaken(savedUri)
|
|
551
|
+
|
|
552
|
+
val saveToGallery = options["savePhoto"] as? Boolean ?: false
|
|
553
|
+
if (saveToGallery) {
|
|
554
|
+
saveImageToGallery(outputFile, context)
|
|
555
|
+
}
|
|
556
|
+
|
|
512
557
|
Log.d(TAG, "CameraView: Photo capture succeeded: $savedUri")
|
|
513
558
|
|
|
514
559
|
val imageInfo = Arguments.createMap()
|
|
515
560
|
imageInfo.putString("uri", uri.toString())
|
|
516
561
|
imageInfo.putString("id", id)
|
|
517
562
|
imageInfo.putString("name", name)
|
|
518
|
-
imageInfo.
|
|
519
|
-
imageInfo.
|
|
563
|
+
imageInfo.putDouble("width", targetWidth ?: width.toDouble())
|
|
564
|
+
imageInfo.putDouble("height", targetHeight ?: height.toDouble())
|
|
520
565
|
imageInfo.putString("path", path)
|
|
521
566
|
|
|
522
567
|
promise.resolve(imageInfo)
|
|
@@ -528,6 +573,42 @@ open class CKCamera(context: ThemedReactContext) : FrameLayout(context), Lifecyc
|
|
|
528
573
|
})
|
|
529
574
|
}
|
|
530
575
|
|
|
576
|
+
private fun saveImageToGallery(imageFile: File, context: Context) {
|
|
577
|
+
val displayName = "CameraKit_Image_${System.currentTimeMillis()}.jpg"
|
|
578
|
+
|
|
579
|
+
val imagesCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
580
|
+
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
581
|
+
} else {
|
|
582
|
+
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
val contentValues = ContentValues().apply {
|
|
586
|
+
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
|
|
587
|
+
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
|
588
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
589
|
+
put(MediaStore.Images.Media.IS_PENDING, 1)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
val resolver = context.contentResolver
|
|
594
|
+
|
|
595
|
+
val imageUri = resolver.insert(imagesCollection, contentValues)
|
|
596
|
+
|
|
597
|
+
imageUri?.let {
|
|
598
|
+
val outputStream = resolver.openOutputStream(it)
|
|
599
|
+
outputStream?.use { stream ->
|
|
600
|
+
imageFile.inputStream().copyTo(stream)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
604
|
+
contentValues.clear()
|
|
605
|
+
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
|
606
|
+
resolver.update(imageUri, contentValues, null, null)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
531
612
|
private fun focusOnPoint(x: Float?, y: Float?) {
|
|
532
613
|
if (x === null || y === null) {
|
|
533
614
|
camera?.cameraControl?.cancelFocusAndMetering()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="react-native/types/modules/codegen" />
|
|
2
|
-
/// <reference types="react-native/types/modules/codegen" />
|
|
3
2
|
import type { ViewProps, ColorValue } from 'react-native';
|
|
4
3
|
import type { DirectEventHandler, Int32, Double, WithDefault, Float } from 'react-native/Libraries/Types/CodegenTypes';
|
|
5
4
|
type OnReadCodeData = {
|
|
@@ -22,7 +22,7 @@ import Foundation
|
|
|
22
22
|
options: NSDictionary,
|
|
23
23
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
24
24
|
reject: @escaping RCTPromiseRejectBlock) {
|
|
25
|
-
camera.capture(onSuccess: { resolve($0) },
|
|
25
|
+
camera.capture(options as! [String: Any], onSuccess: { resolve($0) },
|
|
26
26
|
onError: { reject("capture_error", $0, nil) })
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -311,7 +311,7 @@ public class CameraView: UIView {
|
|
|
311
311
|
|
|
312
312
|
// MARK: Public
|
|
313
313
|
|
|
314
|
-
@objc public func capture(onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
|
314
|
+
@objc public func capture(_ options: [String: Any], onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
|
315
315
|
onError: @escaping (_ error: String) -> Void) {
|
|
316
316
|
camera.capturePicture(onWillCapture: { [weak self] in
|
|
317
317
|
// Flash/dim preview to indicate shutter action
|
|
@@ -326,6 +326,7 @@ public class CameraView: UIView {
|
|
|
326
326
|
self?.writeCaptured(imageData: imageData,
|
|
327
327
|
thumbnailData: thumbnailData,
|
|
328
328
|
dimensions: dimensions,
|
|
329
|
+
options: options,
|
|
329
330
|
onSuccess: onSuccess,
|
|
330
331
|
onError: onError)
|
|
331
332
|
|
|
@@ -428,19 +429,55 @@ public class CameraView: UIView {
|
|
|
428
429
|
private func writeCaptured(imageData: Data,
|
|
429
430
|
thumbnailData: Data?,
|
|
430
431
|
dimensions: CMVideoDimensions,
|
|
432
|
+
options: [String: Any],
|
|
431
433
|
onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
|
432
434
|
onError: @escaping (_ error: String) -> Void) {
|
|
433
435
|
do {
|
|
434
|
-
let temporaryImageFileURL = try saveToTmpFolder(imageData)
|
|
435
|
-
|
|
436
|
-
onSuccess([
|
|
437
|
-
"size": imageData.count,
|
|
438
|
-
"uri": temporaryImageFileURL.description,
|
|
439
|
-
"name": temporaryImageFileURL.lastPathComponent,
|
|
440
|
-
"thumb": "",
|
|
441
|
-
"height": dimensions.height,
|
|
442
|
-
"width": dimensions.width
|
|
443
|
-
])
|
|
436
|
+
// let temporaryImageFileURL = try saveToTmpFolder(imageData)
|
|
437
|
+
//
|
|
438
|
+
// onSuccess([
|
|
439
|
+
// "size": imageData.count,
|
|
440
|
+
// "uri": temporaryImageFileURL.description,
|
|
441
|
+
// "name": temporaryImageFileURL.lastPathComponent,
|
|
442
|
+
// "thumb": "",
|
|
443
|
+
// "height": dimensions.height,
|
|
444
|
+
// "width": dimensions.width
|
|
445
|
+
// ])
|
|
446
|
+
guard var image = UIImage(data: imageData) else {
|
|
447
|
+
onError("Failed to create UIImage from captured data")
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get width, height, quality from options
|
|
452
|
+
let targetWidth = options["width"] as? Int
|
|
453
|
+
let targetHeight = options["height"] as? Int
|
|
454
|
+
let targetQuality = CGFloat((options["quality"] as? Int ?? 100)) / 100.0
|
|
455
|
+
|
|
456
|
+
// Resize image if width and height are specified
|
|
457
|
+
if let width = targetWidth, let height = targetHeight {
|
|
458
|
+
image = resizeImage(image: image, targetSize: CGSize(width: width, height: height))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Convert to JPEG data with specified quality
|
|
462
|
+
guard let processedImageData = image.jpegData(compressionQuality: targetQuality) else {
|
|
463
|
+
onError("Failed to convert processed image to JPEG data")
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let temporaryImageFileURL = try saveToTmpFolder(processedImageData)
|
|
468
|
+
|
|
469
|
+
onSuccess([
|
|
470
|
+
"size": processedImageData.count,
|
|
471
|
+
"uri": temporaryImageFileURL.description,
|
|
472
|
+
"name": temporaryImageFileURL.lastPathComponent,
|
|
473
|
+
"thumb": "",
|
|
474
|
+
"width": targetWidth ?? dimensions.width,
|
|
475
|
+
"height": targetHeight ?? dimensions.height,
|
|
476
|
+
])
|
|
477
|
+
|
|
478
|
+
if let savePhoto = options["savePhoto"] as? Bool, savePhoto {
|
|
479
|
+
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
|
480
|
+
}
|
|
444
481
|
} catch {
|
|
445
482
|
let errorMessage = "Error occurred while writing image data to a temporary file: \(error)"
|
|
446
483
|
print(errorMessage)
|
|
@@ -464,6 +501,36 @@ public class CameraView: UIView {
|
|
|
464
501
|
|
|
465
502
|
return temporaryFileURL
|
|
466
503
|
}
|
|
504
|
+
|
|
505
|
+
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
|
|
506
|
+
let widthRatio = targetSize.width / image.size.width
|
|
507
|
+
let heightRatio = targetSize.height / image.size.height
|
|
508
|
+
|
|
509
|
+
// Chọn scale lớn hơn để đảm bảo ảnh phủ kín targetSize
|
|
510
|
+
let scaleFactor = max(widthRatio, heightRatio)
|
|
511
|
+
|
|
512
|
+
let scaledWidth = image.size.width * scaleFactor
|
|
513
|
+
let scaledHeight = image.size.height * scaleFactor
|
|
514
|
+
|
|
515
|
+
// Resize trước
|
|
516
|
+
UIGraphicsBeginImageContextWithOptions(CGSize(width: scaledWidth, height: scaledHeight), false, 1.0)
|
|
517
|
+
image.draw(in: CGRect(origin: .zero, size: CGSize(width: scaledWidth, height: scaledHeight)))
|
|
518
|
+
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
519
|
+
UIGraphicsEndImageContext()
|
|
520
|
+
|
|
521
|
+
guard let finalScaledImage = scaledImage else { return image }
|
|
522
|
+
|
|
523
|
+
// Crop giữa ảnh để đúng targetSize
|
|
524
|
+
let cropX = (scaledWidth - targetSize.width) / 2
|
|
525
|
+
let cropY = (scaledHeight - targetSize.height) / 2
|
|
526
|
+
let cropRect = CGRect(x: cropX, y: cropY, width: targetSize.width, height: targetSize.height)
|
|
527
|
+
|
|
528
|
+
if let cgImage = finalScaledImage.cgImage?.cropping(to: cropRect) {
|
|
529
|
+
return UIImage(cgImage: cgImage)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return finalScaledImage
|
|
533
|
+
}
|
|
467
534
|
|
|
468
535
|
private func onBarcodeRead(barcode: String, codeFormat:CodeFormat) {
|
|
469
536
|
// Throttle barcode detection
|
|
@@ -46,7 +46,7 @@ RCT_EXPORT_MODULE();
|
|
|
46
46
|
|
|
47
47
|
RCT_EXPORT_METHOD(capture:(NSDictionary *)options tag:(nonnull NSNumber *)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
48
48
|
[self withCamera:tag block:^(CKCameraView *view) {
|
|
49
|
-
[CKCameraManager captureWithCamera:view options:
|
|
49
|
+
[CKCameraManager captureWithCamera:view options:options resolve:resolve reject:reject];
|
|
50
50
|
} reject:reject methodName:@"capture"];
|
|
51
51
|
}
|
|
52
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momo-kits/camerakit",
|
|
3
|
-
"version": "0.150.2-beta.
|
|
3
|
+
"version": "0.150.2-beta.30",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/teslamotors/react-native-camera-kit.git"
|
|
@@ -11,44 +11,16 @@
|
|
|
11
11
|
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
|
12
12
|
"nativePackage": true,
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "
|
|
15
|
-
"clean": "rm -rf dist/",
|
|
14
|
+
"build": "echo",
|
|
16
15
|
"test": "jest",
|
|
17
|
-
"lint": "yarn eslint -c .eslintrc.js"
|
|
18
|
-
"check-ios": "scripts/check-ios.sh",
|
|
19
|
-
"release": "yarn clean && yarn build && yarn publish --verbose",
|
|
20
|
-
"release:beta": "yarn clean && yarn build && yarn publish --tag beta --verbose",
|
|
21
|
-
"release:local": "yarn clean && yarn build && tmp=$(mktemp) && yarn pack --filename $tmp.tar.gz && open -R $tmp.tar.gz",
|
|
22
|
-
"start": "watchman watch-del-all && node node_modules/react-native/local-cli/cli.js start",
|
|
23
|
-
"bootstrap": "cd example/ && bundle install && yarn && cd ios/ && bundle exec pod install",
|
|
24
|
-
"bootstrap-linux": "cd example/ && yarn"
|
|
16
|
+
"lint": "yarn eslint -c .eslintrc.js"
|
|
25
17
|
},
|
|
26
|
-
"main": "
|
|
27
|
-
"module": "dist/index.js",
|
|
28
|
-
"source": "./src/",
|
|
29
|
-
"types": "dist/index.d.ts",
|
|
30
|
-
"react-native": "src/index",
|
|
18
|
+
"main": "./src/index.ts",
|
|
31
19
|
"dependencies": {},
|
|
32
20
|
"license": "MIT",
|
|
33
21
|
"devDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"@babel/runtime": "^7.20.0",
|
|
37
|
-
"@react-native/babel-preset": "0.74.83",
|
|
38
|
-
"@react-native/eslint-config": "0.74.83",
|
|
39
|
-
"@react-native/metro-config": "0.74.83",
|
|
40
|
-
"@react-native/typescript-config": "0.74.83",
|
|
41
|
-
"@types/react": "^18.2.6",
|
|
42
|
-
"@types/react-test-renderer": "^18.0.0",
|
|
43
|
-
"babel-jest": "^29.6.3",
|
|
44
|
-
"eslint": "^8.19.0",
|
|
45
|
-
"eslint-plugin-import": "^2.22.1",
|
|
46
|
-
"jest": "^29.6.3",
|
|
47
|
-
"prettier": "2.8.8",
|
|
48
|
-
"react": "18.2.0",
|
|
49
|
-
"react-native": "0.74.1",
|
|
50
|
-
"react-test-renderer": "18.2.0",
|
|
51
|
-
"typescript": "5.0.4"
|
|
22
|
+
"react": "19.0.0",
|
|
23
|
+
"react-native": "0.80.1"
|
|
52
24
|
},
|
|
53
25
|
"peerDependencies": {
|
|
54
26
|
"react": "*",
|