react-native-attachment-kit 1.0.17 → 1.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/CHANGELOG.md +472 -22
- 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 +60 -341
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +60 -341
- package/ios/RNAttachmentKit.swift +66 -771
- package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
- 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 +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +108 -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 +108 -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/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +108 -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 +108 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +108 -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 +108 -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 +22 -22
- package/package.json +1 -1
- package/src/types.ts +5 -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 가 없을 때) */
|
|
@@ -708,6 +706,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
708
706
|
// 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
|
|
709
707
|
putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
|
|
710
708
|
putBoolean("showDeleteButton", !options.hasKey("showDeleteButton") || options.getBoolean("showDeleteButton"))
|
|
709
|
+
// 처음에 상단바·하단바·동영상 조작 UI 를 숨긴 채로 엶 (탭하면 보임)
|
|
710
|
+
putBoolean("startWithUiHidden", options.hasKey("startWithUiHidden") && options.getBoolean("startWithUiHidden"))
|
|
711
711
|
for (key in listOf(
|
|
712
712
|
"saveAllText", "saveOneText", "shareAllText", "shareOneText", "actionCancelText",
|
|
713
713
|
"deleteConfirmTitle", "deleteConfirmMessage", "deleteConfirmGroupMessage", "deleteButtonText"
|
|
@@ -967,374 +967,93 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
+
/** 고른 뒤 처리 — com.rnattachmentkit.AttachmentProcessor (앱 네이티브 화면과 같은 코드). 여기선 RN 이벤트·HUD 만 */
|
|
970
971
|
private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
|
|
971
|
-
// 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
|
|
972
|
-
val maxWidth = snap.maxWidth
|
|
973
|
-
val maxHeight = snap.maxHeight
|
|
974
|
-
val outputFormat = snap.outputFormat
|
|
975
|
-
val videoOptions = snap.videoOptions
|
|
976
|
-
val lastVideoEditStates = snap.states
|
|
977
|
-
val showVideoHUD = snap.showHUD
|
|
978
|
-
val lastThemeColorInt = snap.themeColor
|
|
979
|
-
val lastLanguageCode = snap.language
|
|
980
|
-
val thumbnailOptions = snap.thumbnail
|
|
981
972
|
val processingId = java.util.UUID.randomUUID().toString()
|
|
982
|
-
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
983
|
-
val contentResolver = reactApplicationContext.contentResolver
|
|
984
973
|
val totalCount = uris.size
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
val hasVideo = uris.any(isVideoUri)
|
|
992
|
-
videoCancelled = false
|
|
974
|
+
val options = com.rnattachmentkit.AttachmentProcessor.Options(
|
|
975
|
+
maxWidth = snap.maxWidth, maxHeight = snap.maxHeight, outputFormat = snap.outputFormat,
|
|
976
|
+
thumbnail = snap.thumbnail, videoOptions = snap.videoOptions,
|
|
977
|
+
)
|
|
978
|
+
val hasVideo = uris.any { com.rnattachmentkit.AttachmentProcessor.isVideoUri(reactApplicationContext, it) }
|
|
979
|
+
// 앱의 cancelVideoProcessing(index) → job.cancel, HUD 의 취소 → job.abort (전체 취소)
|
|
993
980
|
val job = com.rnattachmentkit.ProcessingJob()
|
|
994
981
|
activeProcessingJobs[processingId] = job
|
|
995
982
|
lastProcessingId = processingId
|
|
996
|
-
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
997
|
-
try {
|
|
998
|
-
val items = Arguments.createArray()
|
|
999
|
-
uris.forEachIndexed { i, u ->
|
|
1000
|
-
val it0 = if (isVideoUri(u)) {
|
|
1001
|
-
val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
1002
|
-
com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
|
|
1003
|
-
} else {
|
|
1004
|
-
com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
|
|
1005
|
-
}
|
|
1006
|
-
items.pushMap(Arguments.createMap().apply {
|
|
1007
|
-
putInt("index", it0.index)
|
|
1008
|
-
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1009
|
-
putString("originalUri", it0.originalUri)
|
|
1010
|
-
putInt("width", it0.width)
|
|
1011
|
-
putInt("height", it0.height)
|
|
1012
|
-
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1013
|
-
it0.thumbnailUri?.let { t ->
|
|
1014
|
-
putString("thumbnailUri", t)
|
|
1015
|
-
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1016
|
-
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1017
|
-
}
|
|
1018
|
-
})
|
|
1019
|
-
}
|
|
1020
|
-
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1021
|
-
putString("processingId", processingId)
|
|
1022
|
-
putInt("total", totalCount)
|
|
1023
|
-
putArray("items", items)
|
|
1024
|
-
})
|
|
1025
|
-
} catch (e: Exception) {
|
|
1026
|
-
android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
|
|
1027
|
-
}
|
|
1028
983
|
var hud: com.rnattachmentkit.VideoExportHUD? = null
|
|
1029
|
-
if (hasVideo &&
|
|
984
|
+
if (hasVideo && snap.showHUD) {
|
|
1030
985
|
withContext(Dispatchers.Main) {
|
|
1031
986
|
val act = getCurrentActivity() ?: return@withContext
|
|
1032
|
-
hud = com.rnattachmentkit.VideoExportHUD(act,
|
|
1033
|
-
h.onCancel = {
|
|
1034
|
-
videoCancelled = true
|
|
1035
|
-
currentVideoExporter?.cancel()
|
|
1036
|
-
}
|
|
987
|
+
hud = com.rnattachmentkit.VideoExportHUD(act, snap.themeColor, snap.language).also { h ->
|
|
988
|
+
h.onCancel = { job.abort() }
|
|
1037
989
|
h.show()
|
|
1038
990
|
}
|
|
1039
991
|
}
|
|
1040
992
|
}
|
|
1041
|
-
val
|
|
1042
|
-
|
|
993
|
+
val main = android.os.Handler(android.os.Looper.getMainLooper())
|
|
994
|
+
val emitter = { reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) }
|
|
1043
995
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
|
|
1066
|
-
if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1067
|
-
putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
|
|
996
|
+
com.rnattachmentkit.AttachmentProcessor.process(reactApplicationContext, uris, snap.states, options, job,
|
|
997
|
+
object : com.rnattachmentkit.AttachmentProcessor.Listener {
|
|
998
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
999
|
+
override fun onStart(items: List<com.rnattachmentkit.MediaProcessing.StartItem>) {
|
|
1000
|
+
val arr = Arguments.createArray()
|
|
1001
|
+
items.forEach { it0 ->
|
|
1002
|
+
arr.pushMap(Arguments.createMap().apply {
|
|
1003
|
+
putInt("index", it0.index)
|
|
1004
|
+
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1005
|
+
putString("originalUri", it0.originalUri)
|
|
1006
|
+
putInt("width", it0.width)
|
|
1007
|
+
putInt("height", it0.height)
|
|
1008
|
+
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1009
|
+
it0.thumbnailUri?.let { t ->
|
|
1010
|
+
putString("thumbnailUri", t)
|
|
1011
|
+
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1012
|
+
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1013
|
+
}
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1068
1017
|
putString("processingId", processingId)
|
|
1018
|
+
putInt("total", totalCount)
|
|
1019
|
+
putArray("items", arr)
|
|
1069
1020
|
})
|
|
1070
1021
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1077
|
-
exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
|
|
1078
|
-
}
|
|
1079
|
-
withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
|
|
1080
|
-
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1081
|
-
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1082
|
-
videoFiles += res.file
|
|
1083
|
-
res.thumbnailFile?.let { videoFiles += it }
|
|
1084
|
-
WritableNativeMap().apply {
|
|
1085
|
-
putString("uri", Uri.fromFile(res.file).toString())
|
|
1086
|
-
putString("originalUri", uri.toString())
|
|
1087
|
-
putString("mediaType", "video")
|
|
1088
|
-
putString("type", "video/mp4")
|
|
1089
|
-
putInt("width", res.width)
|
|
1090
|
-
putInt("height", res.height)
|
|
1091
|
-
putDouble("duration", res.durationMs / 1000.0)
|
|
1092
|
-
putDouble("fileSize", res.fileSize.toDouble())
|
|
1093
|
-
putString("fileName", res.file.name)
|
|
1094
|
-
putString("fileExtension", "mp4")
|
|
1095
|
-
putString("videoCodec", res.codec)
|
|
1096
|
-
putInt("videoBitrate", res.bitrate)
|
|
1097
|
-
putBoolean("edited", res.edited)
|
|
1098
|
-
if (res.thumbnailFile != null) {
|
|
1099
|
-
putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
|
|
1100
|
-
putInt("thumbnailWidth", res.thumbnailWidth)
|
|
1101
|
-
putInt("thumbnailHeight", res.thumbnailHeight)
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1105
|
-
// 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
|
|
1106
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
|
|
1107
|
-
} catch (e: Exception) {
|
|
1108
|
-
// 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
|
|
1109
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
|
|
1110
|
-
android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
|
|
1111
|
-
WritableNativeMap().apply {
|
|
1112
|
-
putString("originalUri", uri.toString())
|
|
1113
|
-
putString("mediaType", "video")
|
|
1114
|
-
putString("error", e.message ?: "export failed")
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
} finally {
|
|
1118
|
-
currentVideoExporter = null
|
|
1119
|
-
job.end()
|
|
1120
|
-
}
|
|
1121
|
-
results.add(resultMap)
|
|
1122
|
-
try {
|
|
1123
|
-
val copy = WritableNativeMap().apply { merge(resultMap) }
|
|
1124
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1125
|
-
.emit("onImageProcessed", WritableNativeMap().apply {
|
|
1126
|
-
putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
|
|
1022
|
+
|
|
1023
|
+
override fun onVideoProgress(index: Int, progress: Float) {
|
|
1024
|
+
hud?.let { h -> main.post { h.update(progress, index, totalCount) } }
|
|
1025
|
+
emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1026
|
+
putInt("index", index); putInt("total", totalCount); putDouble("progress", progress.toDouble())
|
|
1127
1027
|
putString("processingId", processingId)
|
|
1128
1028
|
})
|
|
1129
|
-
} catch (_: Exception) {}
|
|
1130
|
-
currentIndex++
|
|
1131
|
-
continue
|
|
1132
|
-
}
|
|
1133
|
-
try {
|
|
1134
|
-
// 1. Get EXIF Orientation
|
|
1135
|
-
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
1136
|
-
try {
|
|
1137
|
-
contentResolver.openInputStream(uri)?.use { exifInputStream ->
|
|
1138
|
-
val exif = android.media.ExifInterface(exifInputStream)
|
|
1139
|
-
orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
|
|
1140
1029
|
}
|
|
1141
|
-
} catch (e: Exception) {}
|
|
1142
1030
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
var originalHeight = originalOptions.outHeight
|
|
1152
|
-
|
|
1153
|
-
// Swap width and height if rotated by 90 or 270 degrees
|
|
1154
|
-
if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
|
|
1155
|
-
val temp = originalWidth
|
|
1156
|
-
originalWidth = originalHeight
|
|
1157
|
-
originalHeight = temp
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Load and resize image
|
|
1161
|
-
contentResolver.openInputStream(uri)?.use { resizedInputStream ->
|
|
1162
|
-
// Initial decode for sizing
|
|
1163
|
-
val decodeOptions = BitmapFactory.Options().apply {
|
|
1164
|
-
inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
|
|
1165
|
-
}
|
|
1166
|
-
var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
|
|
1167
|
-
|
|
1168
|
-
if (bitmap != null) {
|
|
1169
|
-
// Apply EXIF Rotation
|
|
1170
|
-
val matrix = android.graphics.Matrix()
|
|
1171
|
-
when (orientation) {
|
|
1172
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
|
1173
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
|
1174
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
|
1175
|
-
android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
|
|
1176
|
-
android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
if (!matrix.isIdentity) {
|
|
1180
|
-
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
1181
|
-
if (rotatedBitmap != bitmap) {
|
|
1182
|
-
bitmap.recycle()
|
|
1183
|
-
bitmap = rotatedBitmap
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Precise resize if needed
|
|
1188
|
-
val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
|
|
1189
|
-
resizeBitmap(bitmap, maxWidth, maxHeight)
|
|
1190
|
-
} else {
|
|
1191
|
-
bitmap
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
|
|
1195
|
-
|
|
1196
|
-
// Determine format and extension
|
|
1197
|
-
val compressFormat: Bitmap.CompressFormat
|
|
1198
|
-
val extension: String
|
|
1199
|
-
val mimeType: String
|
|
1200
|
-
|
|
1201
|
-
when (outputFormat) {
|
|
1202
|
-
"png" -> {
|
|
1203
|
-
compressFormat = Bitmap.CompressFormat.PNG
|
|
1204
|
-
extension = "png"
|
|
1205
|
-
mimeType = "image/png"
|
|
1206
|
-
}
|
|
1207
|
-
"jpg", "jpeg" -> {
|
|
1208
|
-
compressFormat = Bitmap.CompressFormat.JPEG
|
|
1209
|
-
extension = "jpg"
|
|
1210
|
-
mimeType = "image/jpeg"
|
|
1211
|
-
}
|
|
1212
|
-
else -> { // webp
|
|
1213
|
-
compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1214
|
-
Bitmap.CompressFormat.WEBP_LOSSY
|
|
1215
|
-
} else {
|
|
1216
|
-
@Suppress("DEPRECATION")
|
|
1217
|
-
Bitmap.CompressFormat.WEBP
|
|
1218
|
-
}
|
|
1219
|
-
extension = "webp"
|
|
1220
|
-
mimeType = "image/webp"
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Save to temporary file
|
|
1225
|
-
val tempFile = File.createTempFile("turbo_edited_${System.currentTimeMillis()}_", ".$extension", reactApplicationContext.cacheDir)
|
|
1226
|
-
FileOutputStream(tempFile).use { out ->
|
|
1227
|
-
processedBitmap.compress(compressFormat, 85, out)
|
|
1228
|
-
}
|
|
1229
|
-
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서 — 원본을 다시 읽지 않음
|
|
1230
|
-
val thumb = thumbnailOptions?.let {
|
|
1231
|
-
com.rnattachmentkit.PhotoThumbnail.make(processedBitmap, it, reactApplicationContext.cacheDir, currentIndex)
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
val finalWidth = processedBitmap.width
|
|
1235
|
-
val finalHeight = processedBitmap.height
|
|
1236
|
-
|
|
1237
|
-
// Cleanup
|
|
1238
|
-
if (bitmap != processedBitmap) {
|
|
1239
|
-
bitmap.recycle()
|
|
1240
|
-
}
|
|
1241
|
-
if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
|
|
1242
|
-
finalBitmap.recycle()
|
|
1243
|
-
}
|
|
1244
|
-
processedBitmap.recycle()
|
|
1245
|
-
|
|
1246
|
-
val createImageInfo = {
|
|
1247
|
-
WritableNativeMap().apply {
|
|
1248
|
-
putString("uri", Uri.fromFile(tempFile).toString())
|
|
1249
|
-
putInt("width", finalWidth)
|
|
1250
|
-
putInt("height", finalHeight)
|
|
1251
|
-
putString("type", mimeType)
|
|
1252
|
-
putString("fileName", tempFile.name)
|
|
1253
|
-
putString("fileExtension", extension)
|
|
1254
|
-
putDouble("fileSize", tempFile.length().toDouble())
|
|
1255
|
-
putString("originalUri", uri.toString())
|
|
1256
|
-
putInt("originalWidth", originalWidth)
|
|
1257
|
-
putInt("originalHeight", originalHeight)
|
|
1258
|
-
if (thumb != null) {
|
|
1259
|
-
putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
|
|
1260
|
-
putInt("thumbnailWidth", thumb.width)
|
|
1261
|
-
putInt("thumbnailHeight", thumb.height)
|
|
1262
|
-
putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
val imageInfo = createImageInfo()
|
|
1267
|
-
results.add(imageInfo)
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
val eventMap = WritableNativeMap().apply {
|
|
1271
|
-
putInt("total", totalCount)
|
|
1272
|
-
putInt("index", currentIndex)
|
|
1273
|
-
putString("processingId", processingId)
|
|
1274
|
-
putMap("image", createImageInfo())
|
|
1275
|
-
}
|
|
1276
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1277
|
-
.emit("onImageProcessed", eventMap)
|
|
1278
|
-
} catch (e: Exception) { }
|
|
1279
|
-
}
|
|
1031
|
+
override fun onItem(result: com.rnattachmentkit.AttachmentProcessor.Result) {
|
|
1032
|
+
if (result.isVideo) hud?.let { h -> main.post { h.update(1f, result.index, totalCount) } }
|
|
1033
|
+
try {
|
|
1034
|
+
emitter().emit("onImageProcessed", WritableNativeMap().apply {
|
|
1035
|
+
putInt("total", totalCount); putInt("index", result.index); putMap("image", toWritableMap(result))
|
|
1036
|
+
putString("processingId", processingId)
|
|
1037
|
+
})
|
|
1038
|
+
} catch (_: Exception) {}
|
|
1280
1039
|
}
|
|
1281
|
-
}
|
|
1282
|
-
currentIndex++
|
|
1283
|
-
} catch (e: Exception) {
|
|
1284
|
-
currentIndex++
|
|
1285
|
-
// Skip failed images
|
|
1286
|
-
continue
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1290
|
-
// 취소: 지금까지 만든 동영상 파일은 지움
|
|
1291
|
-
videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
|
|
1292
|
-
throw e
|
|
1040
|
+
}).map { toWritableMap(it) }
|
|
1293
1041
|
} finally {
|
|
1294
1042
|
activeProcessingJobs.remove(processingId)
|
|
1295
1043
|
if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
|
|
1296
1044
|
}
|
|
1297
|
-
|
|
1298
|
-
results
|
|
1299
1045
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (reqHeight > maxHeight || reqWidth > maxWidth) {
|
|
1310
|
-
val halfHeight = reqHeight / 2
|
|
1311
|
-
val halfWidth = reqWidth / 2
|
|
1312
|
-
|
|
1313
|
-
while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
|
|
1314
|
-
inSampleSize *= 2
|
|
1046
|
+
|
|
1047
|
+
/** AttachmentProcessor.Result → RN 결과 맵 (예전 브릿지와 같은 필드·타입) */
|
|
1048
|
+
private fun toWritableMap(r: com.rnattachmentkit.AttachmentProcessor.Result): WritableMap = WritableNativeMap().apply {
|
|
1049
|
+
r.toMap().forEach { (k, v) ->
|
|
1050
|
+
when (v) {
|
|
1051
|
+
is Boolean -> putBoolean(k, v)
|
|
1052
|
+
is Int -> putInt(k, v)
|
|
1053
|
+
is Number -> putDouble(k, v.toDouble())
|
|
1054
|
+
else -> putString(k, v.toString())
|
|
1315
1055
|
}
|
|
1316
1056
|
}
|
|
1317
|
-
|
|
1318
|
-
return inSampleSize
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
|
|
1322
|
-
val width = bitmap.width
|
|
1323
|
-
val height = bitmap.height
|
|
1324
|
-
|
|
1325
|
-
val ratioBitmap = width.toFloat() / height.toFloat()
|
|
1326
|
-
val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
|
|
1327
|
-
|
|
1328
|
-
var finalWidth = maxWidth
|
|
1329
|
-
var finalHeight = maxHeight
|
|
1330
|
-
|
|
1331
|
-
if (ratioMax > ratioBitmap) {
|
|
1332
|
-
finalWidth = (maxHeight * ratioBitmap).toInt()
|
|
1333
|
-
} else {
|
|
1334
|
-
finalHeight = (maxWidth / ratioBitmap).toInt()
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
|
|
1338
1057
|
}
|
|
1339
1058
|
|
|
1340
1059
|
|