react-native-attachment-kit 1.0.18 → 1.1.1
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/CHANGELOG.md +15 -0
- package/README.md +4 -2
- package/android/build.gradle +8 -3
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +82 -341
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +82 -341
- package/ios/RNAttachmentKit.swift +84 -771
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +6709 -449
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6709 -449
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6709 -449
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +110 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +20 -20
- package/package.json +1 -1
- package/src/index.ts +4 -3
- package/src/types.ts +28 -0
|
@@ -46,8 +46,6 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
46
46
|
private var lastMaxSelection: Int = 10
|
|
47
47
|
private var lastThemeColorInt: Int? = null
|
|
48
48
|
private var lastLanguageCode: String = "en"
|
|
49
|
-
@Volatile private var currentVideoExporter: com.rnattachmentkit.VideoExporter? = null
|
|
50
|
-
@Volatile private var videoCancelled = false
|
|
51
49
|
/** 진행 중인 전송 처리 (processingId → 작업). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음 */
|
|
52
50
|
private val activeProcessingJobs = java.util.concurrent.ConcurrentHashMap<String, com.rnattachmentkit.ProcessingJob>()
|
|
53
51
|
/** 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때) */
|
|
@@ -524,10 +522,31 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
524
522
|
intent.putStringArrayListExtra("sourceBorderCorners", corners)
|
|
525
523
|
}
|
|
526
524
|
}
|
|
525
|
+
// 잘라내기 영역 — 넘긴 키만 바뀜 (useOverlayViewer 뷰어만 반영)
|
|
526
|
+
clipArrayOf(options, "sourceClipRect")?.let { intent.putExtra("sourceClipRect", it) }
|
|
527
|
+
clipArrayOf(options, "sourceClipInsets")?.let { intent.putExtra("sourceClipInsets", it) }
|
|
527
528
|
androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext).sendBroadcast(intent)
|
|
528
529
|
promise.resolve(null)
|
|
529
530
|
}
|
|
530
531
|
|
|
532
|
+
/**
|
|
533
|
+
* sourceClipRect {x,y,width,height} / sourceClipInsets {top,bottom,left,right} (dp) → 화면 px [l,t,r,b]
|
|
534
|
+
* (여백이면 각 변의 여백). 키가 없으면 null(안 바꿈), 값이 null 이면 빈 배열(자르지 않음).
|
|
535
|
+
*/
|
|
536
|
+
private fun clipArrayOf(options: ReadableMap, key: String): FloatArray? {
|
|
537
|
+
if (!options.hasKey(key)) return null
|
|
538
|
+
if (options.isNull(key)) return FloatArray(0)
|
|
539
|
+
val m = options.getMap(key) ?: return FloatArray(0)
|
|
540
|
+
fun px(k: String) = if (m.hasKey(k) && !m.isNull(k)) com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(m.getDouble(k).toFloat()) else 0f
|
|
541
|
+
return if (key == "sourceClipRect") {
|
|
542
|
+
val x = px("x")
|
|
543
|
+
val y = px("y")
|
|
544
|
+
floatArrayOf(x, y, x + px("width"), y + px("height"))
|
|
545
|
+
} else {
|
|
546
|
+
floatArrayOf(px("left"), px("top"), px("right"), px("bottom"))
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
/** 동영상 변환 취소 (HUD 없이 앱이 말풍선에서). index -1 = 남은 동영상 전부. 진행 중인 처리가 없으면 false */
|
|
532
551
|
@ReactMethod
|
|
533
552
|
fun cancelVideoProcessing(index: Double, processingId: String, promise: Promise) {
|
|
@@ -675,6 +694,9 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
675
694
|
if (options.hasKey("sourceBackgroundColor")) {
|
|
676
695
|
putString("sourceBackgroundColor", options.getString("sourceBackgroundColor"))
|
|
677
696
|
}
|
|
697
|
+
// 잘라내기 영역 (열림·닫힘 중 사진과 가림 판이 그려지는 곳) — 이 오버레이 뷰어만
|
|
698
|
+
clipArrayOf(options, "sourceClipRect")?.let { putFloatArray("sourceClipRect", it) }
|
|
699
|
+
clipArrayOf(options, "sourceClipInsets")?.let { putFloatArray("sourceClipInsets", it) }
|
|
678
700
|
// placeholderImages: 타입/뷰어 코드에는 있었지만 브릿지에서 한 번도 전달되지 않았던
|
|
679
701
|
// 필드. 네이티브 뷰어가 onlyRetrieveFromCache(true)로 첫 프레임을 즉시 그리려 할 때
|
|
680
702
|
// 쓰는 저해상도 대체 이미지 목록입니다(원본 다운로드 전 임시 표시용).
|
|
@@ -969,374 +991,93 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
969
991
|
}
|
|
970
992
|
}
|
|
971
993
|
|
|
994
|
+
/** 고른 뒤 처리 — com.rnattachmentkit.AttachmentProcessor (앱 네이티브 화면과 같은 코드). 여기선 RN 이벤트·HUD 만 */
|
|
972
995
|
private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
|
|
973
|
-
// 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
|
|
974
|
-
val maxWidth = snap.maxWidth
|
|
975
|
-
val maxHeight = snap.maxHeight
|
|
976
|
-
val outputFormat = snap.outputFormat
|
|
977
|
-
val videoOptions = snap.videoOptions
|
|
978
|
-
val lastVideoEditStates = snap.states
|
|
979
|
-
val showVideoHUD = snap.showHUD
|
|
980
|
-
val lastThemeColorInt = snap.themeColor
|
|
981
|
-
val lastLanguageCode = snap.language
|
|
982
|
-
val thumbnailOptions = snap.thumbnail
|
|
983
996
|
val processingId = java.util.UUID.randomUUID().toString()
|
|
984
|
-
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
985
|
-
val contentResolver = reactApplicationContext.contentResolver
|
|
986
997
|
val totalCount = uris.size
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
val hasVideo = uris.any(isVideoUri)
|
|
994
|
-
videoCancelled = false
|
|
998
|
+
val options = com.rnattachmentkit.AttachmentProcessor.Options(
|
|
999
|
+
maxWidth = snap.maxWidth, maxHeight = snap.maxHeight, outputFormat = snap.outputFormat,
|
|
1000
|
+
thumbnail = snap.thumbnail, videoOptions = snap.videoOptions,
|
|
1001
|
+
)
|
|
1002
|
+
val hasVideo = uris.any { com.rnattachmentkit.AttachmentProcessor.isVideoUri(reactApplicationContext, it) }
|
|
1003
|
+
// 앱의 cancelVideoProcessing(index) → job.cancel, HUD 의 취소 → job.abort (전체 취소)
|
|
995
1004
|
val job = com.rnattachmentkit.ProcessingJob()
|
|
996
1005
|
activeProcessingJobs[processingId] = job
|
|
997
1006
|
lastProcessingId = processingId
|
|
998
|
-
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
999
|
-
try {
|
|
1000
|
-
val items = Arguments.createArray()
|
|
1001
|
-
uris.forEachIndexed { i, u ->
|
|
1002
|
-
val it0 = if (isVideoUri(u)) {
|
|
1003
|
-
val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
1004
|
-
com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
|
|
1005
|
-
} else {
|
|
1006
|
-
com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
|
|
1007
|
-
}
|
|
1008
|
-
items.pushMap(Arguments.createMap().apply {
|
|
1009
|
-
putInt("index", it0.index)
|
|
1010
|
-
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1011
|
-
putString("originalUri", it0.originalUri)
|
|
1012
|
-
putInt("width", it0.width)
|
|
1013
|
-
putInt("height", it0.height)
|
|
1014
|
-
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1015
|
-
it0.thumbnailUri?.let { t ->
|
|
1016
|
-
putString("thumbnailUri", t)
|
|
1017
|
-
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1018
|
-
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1019
|
-
}
|
|
1020
|
-
})
|
|
1021
|
-
}
|
|
1022
|
-
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1023
|
-
putString("processingId", processingId)
|
|
1024
|
-
putInt("total", totalCount)
|
|
1025
|
-
putArray("items", items)
|
|
1026
|
-
})
|
|
1027
|
-
} catch (e: Exception) {
|
|
1028
|
-
android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
|
|
1029
|
-
}
|
|
1030
1007
|
var hud: com.rnattachmentkit.VideoExportHUD? = null
|
|
1031
|
-
if (hasVideo &&
|
|
1008
|
+
if (hasVideo && snap.showHUD) {
|
|
1032
1009
|
withContext(Dispatchers.Main) {
|
|
1033
1010
|
val act = getCurrentActivity() ?: return@withContext
|
|
1034
|
-
hud = com.rnattachmentkit.VideoExportHUD(act,
|
|
1035
|
-
h.onCancel = {
|
|
1036
|
-
videoCancelled = true
|
|
1037
|
-
currentVideoExporter?.cancel()
|
|
1038
|
-
}
|
|
1011
|
+
hud = com.rnattachmentkit.VideoExportHUD(act, snap.themeColor, snap.language).also { h ->
|
|
1012
|
+
h.onCancel = { job.abort() }
|
|
1039
1013
|
h.show()
|
|
1040
1014
|
}
|
|
1041
1015
|
}
|
|
1042
1016
|
}
|
|
1043
|
-
val
|
|
1044
|
-
|
|
1017
|
+
val main = android.os.Handler(android.os.Looper.getMainLooper())
|
|
1018
|
+
val emitter = { reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) }
|
|
1045
1019
|
try {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
|
|
1068
|
-
if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1069
|
-
putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
|
|
1020
|
+
com.rnattachmentkit.AttachmentProcessor.process(reactApplicationContext, uris, snap.states, options, job,
|
|
1021
|
+
object : com.rnattachmentkit.AttachmentProcessor.Listener {
|
|
1022
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
1023
|
+
override fun onStart(items: List<com.rnattachmentkit.MediaProcessing.StartItem>) {
|
|
1024
|
+
val arr = Arguments.createArray()
|
|
1025
|
+
items.forEach { it0 ->
|
|
1026
|
+
arr.pushMap(Arguments.createMap().apply {
|
|
1027
|
+
putInt("index", it0.index)
|
|
1028
|
+
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1029
|
+
putString("originalUri", it0.originalUri)
|
|
1030
|
+
putInt("width", it0.width)
|
|
1031
|
+
putInt("height", it0.height)
|
|
1032
|
+
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1033
|
+
it0.thumbnailUri?.let { t ->
|
|
1034
|
+
putString("thumbnailUri", t)
|
|
1035
|
+
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1036
|
+
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1037
|
+
}
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1070
1041
|
putString("processingId", processingId)
|
|
1042
|
+
putInt("total", totalCount)
|
|
1043
|
+
putArray("items", arr)
|
|
1071
1044
|
})
|
|
1072
1045
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1079
|
-
exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
|
|
1080
|
-
}
|
|
1081
|
-
withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
|
|
1082
|
-
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1083
|
-
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1084
|
-
videoFiles += res.file
|
|
1085
|
-
res.thumbnailFile?.let { videoFiles += it }
|
|
1086
|
-
WritableNativeMap().apply {
|
|
1087
|
-
putString("uri", Uri.fromFile(res.file).toString())
|
|
1088
|
-
putString("originalUri", uri.toString())
|
|
1089
|
-
putString("mediaType", "video")
|
|
1090
|
-
putString("type", "video/mp4")
|
|
1091
|
-
putInt("width", res.width)
|
|
1092
|
-
putInt("height", res.height)
|
|
1093
|
-
putDouble("duration", res.durationMs / 1000.0)
|
|
1094
|
-
putDouble("fileSize", res.fileSize.toDouble())
|
|
1095
|
-
putString("fileName", res.file.name)
|
|
1096
|
-
putString("fileExtension", "mp4")
|
|
1097
|
-
putString("videoCodec", res.codec)
|
|
1098
|
-
putInt("videoBitrate", res.bitrate)
|
|
1099
|
-
putBoolean("edited", res.edited)
|
|
1100
|
-
if (res.thumbnailFile != null) {
|
|
1101
|
-
putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
|
|
1102
|
-
putInt("thumbnailWidth", res.thumbnailWidth)
|
|
1103
|
-
putInt("thumbnailHeight", res.thumbnailHeight)
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1107
|
-
// 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
|
|
1108
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
|
|
1109
|
-
} catch (e: Exception) {
|
|
1110
|
-
// 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
|
|
1111
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
|
|
1112
|
-
android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
|
|
1113
|
-
WritableNativeMap().apply {
|
|
1114
|
-
putString("originalUri", uri.toString())
|
|
1115
|
-
putString("mediaType", "video")
|
|
1116
|
-
putString("error", e.message ?: "export failed")
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
} finally {
|
|
1120
|
-
currentVideoExporter = null
|
|
1121
|
-
job.end()
|
|
1122
|
-
}
|
|
1123
|
-
results.add(resultMap)
|
|
1124
|
-
try {
|
|
1125
|
-
val copy = WritableNativeMap().apply { merge(resultMap) }
|
|
1126
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1127
|
-
.emit("onImageProcessed", WritableNativeMap().apply {
|
|
1128
|
-
putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
|
|
1046
|
+
|
|
1047
|
+
override fun onVideoProgress(index: Int, progress: Float) {
|
|
1048
|
+
hud?.let { h -> main.post { h.update(progress, index, totalCount) } }
|
|
1049
|
+
emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1050
|
+
putInt("index", index); putInt("total", totalCount); putDouble("progress", progress.toDouble())
|
|
1129
1051
|
putString("processingId", processingId)
|
|
1130
1052
|
})
|
|
1131
|
-
} catch (_: Exception) {}
|
|
1132
|
-
currentIndex++
|
|
1133
|
-
continue
|
|
1134
|
-
}
|
|
1135
|
-
try {
|
|
1136
|
-
// 1. Get EXIF Orientation
|
|
1137
|
-
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
1138
|
-
try {
|
|
1139
|
-
contentResolver.openInputStream(uri)?.use { exifInputStream ->
|
|
1140
|
-
val exif = android.media.ExifInterface(exifInputStream)
|
|
1141
|
-
orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
|
|
1142
1053
|
}
|
|
1143
|
-
} catch (e: Exception) {}
|
|
1144
1054
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
var originalHeight = originalOptions.outHeight
|
|
1154
|
-
|
|
1155
|
-
// Swap width and height if rotated by 90 or 270 degrees
|
|
1156
|
-
if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
|
|
1157
|
-
val temp = originalWidth
|
|
1158
|
-
originalWidth = originalHeight
|
|
1159
|
-
originalHeight = temp
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Load and resize image
|
|
1163
|
-
contentResolver.openInputStream(uri)?.use { resizedInputStream ->
|
|
1164
|
-
// Initial decode for sizing
|
|
1165
|
-
val decodeOptions = BitmapFactory.Options().apply {
|
|
1166
|
-
inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
|
|
1167
|
-
}
|
|
1168
|
-
var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
|
|
1169
|
-
|
|
1170
|
-
if (bitmap != null) {
|
|
1171
|
-
// Apply EXIF Rotation
|
|
1172
|
-
val matrix = android.graphics.Matrix()
|
|
1173
|
-
when (orientation) {
|
|
1174
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
|
1175
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
|
1176
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
|
1177
|
-
android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
|
|
1178
|
-
android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
if (!matrix.isIdentity) {
|
|
1182
|
-
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
1183
|
-
if (rotatedBitmap != bitmap) {
|
|
1184
|
-
bitmap.recycle()
|
|
1185
|
-
bitmap = rotatedBitmap
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// Precise resize if needed
|
|
1190
|
-
val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
|
|
1191
|
-
resizeBitmap(bitmap, maxWidth, maxHeight)
|
|
1192
|
-
} else {
|
|
1193
|
-
bitmap
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
|
|
1197
|
-
|
|
1198
|
-
// Determine format and extension
|
|
1199
|
-
val compressFormat: Bitmap.CompressFormat
|
|
1200
|
-
val extension: String
|
|
1201
|
-
val mimeType: String
|
|
1202
|
-
|
|
1203
|
-
when (outputFormat) {
|
|
1204
|
-
"png" -> {
|
|
1205
|
-
compressFormat = Bitmap.CompressFormat.PNG
|
|
1206
|
-
extension = "png"
|
|
1207
|
-
mimeType = "image/png"
|
|
1208
|
-
}
|
|
1209
|
-
"jpg", "jpeg" -> {
|
|
1210
|
-
compressFormat = Bitmap.CompressFormat.JPEG
|
|
1211
|
-
extension = "jpg"
|
|
1212
|
-
mimeType = "image/jpeg"
|
|
1213
|
-
}
|
|
1214
|
-
else -> { // webp
|
|
1215
|
-
compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1216
|
-
Bitmap.CompressFormat.WEBP_LOSSY
|
|
1217
|
-
} else {
|
|
1218
|
-
@Suppress("DEPRECATION")
|
|
1219
|
-
Bitmap.CompressFormat.WEBP
|
|
1220
|
-
}
|
|
1221
|
-
extension = "webp"
|
|
1222
|
-
mimeType = "image/webp"
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// Save to temporary file
|
|
1227
|
-
val tempFile = File.createTempFile("turbo_edited_${System.currentTimeMillis()}_", ".$extension", reactApplicationContext.cacheDir)
|
|
1228
|
-
FileOutputStream(tempFile).use { out ->
|
|
1229
|
-
processedBitmap.compress(compressFormat, 85, out)
|
|
1230
|
-
}
|
|
1231
|
-
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서 — 원본을 다시 읽지 않음
|
|
1232
|
-
val thumb = thumbnailOptions?.let {
|
|
1233
|
-
com.rnattachmentkit.PhotoThumbnail.make(processedBitmap, it, reactApplicationContext.cacheDir, currentIndex)
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
val finalWidth = processedBitmap.width
|
|
1237
|
-
val finalHeight = processedBitmap.height
|
|
1238
|
-
|
|
1239
|
-
// Cleanup
|
|
1240
|
-
if (bitmap != processedBitmap) {
|
|
1241
|
-
bitmap.recycle()
|
|
1242
|
-
}
|
|
1243
|
-
if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
|
|
1244
|
-
finalBitmap.recycle()
|
|
1245
|
-
}
|
|
1246
|
-
processedBitmap.recycle()
|
|
1247
|
-
|
|
1248
|
-
val createImageInfo = {
|
|
1249
|
-
WritableNativeMap().apply {
|
|
1250
|
-
putString("uri", Uri.fromFile(tempFile).toString())
|
|
1251
|
-
putInt("width", finalWidth)
|
|
1252
|
-
putInt("height", finalHeight)
|
|
1253
|
-
putString("type", mimeType)
|
|
1254
|
-
putString("fileName", tempFile.name)
|
|
1255
|
-
putString("fileExtension", extension)
|
|
1256
|
-
putDouble("fileSize", tempFile.length().toDouble())
|
|
1257
|
-
putString("originalUri", uri.toString())
|
|
1258
|
-
putInt("originalWidth", originalWidth)
|
|
1259
|
-
putInt("originalHeight", originalHeight)
|
|
1260
|
-
if (thumb != null) {
|
|
1261
|
-
putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
|
|
1262
|
-
putInt("thumbnailWidth", thumb.width)
|
|
1263
|
-
putInt("thumbnailHeight", thumb.height)
|
|
1264
|
-
putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
val imageInfo = createImageInfo()
|
|
1269
|
-
results.add(imageInfo)
|
|
1270
|
-
|
|
1271
|
-
try {
|
|
1272
|
-
val eventMap = WritableNativeMap().apply {
|
|
1273
|
-
putInt("total", totalCount)
|
|
1274
|
-
putInt("index", currentIndex)
|
|
1275
|
-
putString("processingId", processingId)
|
|
1276
|
-
putMap("image", createImageInfo())
|
|
1277
|
-
}
|
|
1278
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1279
|
-
.emit("onImageProcessed", eventMap)
|
|
1280
|
-
} catch (e: Exception) { }
|
|
1281
|
-
}
|
|
1055
|
+
override fun onItem(result: com.rnattachmentkit.AttachmentProcessor.Result) {
|
|
1056
|
+
if (result.isVideo) hud?.let { h -> main.post { h.update(1f, result.index, totalCount) } }
|
|
1057
|
+
try {
|
|
1058
|
+
emitter().emit("onImageProcessed", WritableNativeMap().apply {
|
|
1059
|
+
putInt("total", totalCount); putInt("index", result.index); putMap("image", toWritableMap(result))
|
|
1060
|
+
putString("processingId", processingId)
|
|
1061
|
+
})
|
|
1062
|
+
} catch (_: Exception) {}
|
|
1282
1063
|
}
|
|
1283
|
-
}
|
|
1284
|
-
currentIndex++
|
|
1285
|
-
} catch (e: Exception) {
|
|
1286
|
-
currentIndex++
|
|
1287
|
-
// Skip failed images
|
|
1288
|
-
continue
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1292
|
-
// 취소: 지금까지 만든 동영상 파일은 지움
|
|
1293
|
-
videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
|
|
1294
|
-
throw e
|
|
1064
|
+
}).map { toWritableMap(it) }
|
|
1295
1065
|
} finally {
|
|
1296
1066
|
activeProcessingJobs.remove(processingId)
|
|
1297
1067
|
if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
|
|
1298
1068
|
}
|
|
1299
|
-
|
|
1300
|
-
results
|
|
1301
1069
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
if (reqHeight > maxHeight || reqWidth > maxWidth) {
|
|
1312
|
-
val halfHeight = reqHeight / 2
|
|
1313
|
-
val halfWidth = reqWidth / 2
|
|
1314
|
-
|
|
1315
|
-
while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
|
|
1316
|
-
inSampleSize *= 2
|
|
1070
|
+
|
|
1071
|
+
/** AttachmentProcessor.Result → RN 결과 맵 (예전 브릿지와 같은 필드·타입) */
|
|
1072
|
+
private fun toWritableMap(r: com.rnattachmentkit.AttachmentProcessor.Result): WritableMap = WritableNativeMap().apply {
|
|
1073
|
+
r.toMap().forEach { (k, v) ->
|
|
1074
|
+
when (v) {
|
|
1075
|
+
is Boolean -> putBoolean(k, v)
|
|
1076
|
+
is Int -> putInt(k, v)
|
|
1077
|
+
is Number -> putDouble(k, v.toDouble())
|
|
1078
|
+
else -> putString(k, v.toString())
|
|
1317
1079
|
}
|
|
1318
1080
|
}
|
|
1319
|
-
|
|
1320
|
-
return inSampleSize
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
|
|
1324
|
-
val width = bitmap.width
|
|
1325
|
-
val height = bitmap.height
|
|
1326
|
-
|
|
1327
|
-
val ratioBitmap = width.toFloat() / height.toFloat()
|
|
1328
|
-
val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
|
|
1329
|
-
|
|
1330
|
-
var finalWidth = maxWidth
|
|
1331
|
-
var finalHeight = maxHeight
|
|
1332
|
-
|
|
1333
|
-
if (ratioMax > ratioBitmap) {
|
|
1334
|
-
finalWidth = (maxHeight * ratioBitmap).toInt()
|
|
1335
|
-
} else {
|
|
1336
|
-
finalHeight = (maxWidth / ratioBitmap).toInt()
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
|
|
1340
1081
|
}
|
|
1341
1082
|
|
|
1342
1083
|
|