react-native-attachment-kit 1.0.18 → 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.
Files changed (28) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/android/build.gradle +8 -3
  3. package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
  4. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +58 -341
  5. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +58 -341
  6. package/ios/RNAttachmentKit.swift +64 -771
  7. package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
  8. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
  9. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +6419 -422
  10. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +107 -0
  11. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  12. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +107 -0
  13. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  14. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  15. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
  16. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  17. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6419 -422
  18. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +107 -0
  19. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  20. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +107 -0
  21. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6419 -422
  22. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +107 -0
  23. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +107 -0
  25. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  26. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  27. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +20 -20
  28. package/package.json +1 -1
@@ -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 가 없을 때) */
@@ -969,374 +967,93 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
969
967
  }
970
968
  }
971
969
 
970
+ /** 고른 뒤 처리 — com.rnattachmentkit.AttachmentProcessor (앱 네이티브 화면과 같은 코드). 여기선 RN 이벤트·HUD 만 */
972
971
  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
972
  val processingId = java.util.UUID.randomUUID().toString()
984
- val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
985
- val contentResolver = reactApplicationContext.contentResolver
986
973
  val totalCount = uris.size
987
- var currentIndex = 0
988
-
989
- // 동영상이 섞여 있으면 진행 HUD 를 띄우고, 동영상은 VideoExporter 로 (사진 처리와 같은 루프)
990
- val isVideoUri: (Uri) -> Boolean = { u ->
991
- u.toString().contains("/video/") || (try { contentResolver.getType(u)?.startsWith("video/") == true } catch (_: Exception) { false })
992
- }
993
- val hasVideo = uris.any(isVideoUri)
994
- 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 (전체 취소)
995
980
  val job = com.rnattachmentkit.ProcessingJob()
996
981
  activeProcessingJobs[processingId] = job
997
982
  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
983
  var hud: com.rnattachmentkit.VideoExportHUD? = null
1031
- if (hasVideo && showVideoHUD) {
984
+ if (hasVideo && snap.showHUD) {
1032
985
  withContext(Dispatchers.Main) {
1033
986
  val act = getCurrentActivity() ?: return@withContext
1034
- hud = com.rnattachmentkit.VideoExportHUD(act, lastThemeColorInt, lastLanguageCode).also { h ->
1035
- h.onCancel = {
1036
- videoCancelled = true
1037
- currentVideoExporter?.cancel()
1038
- }
987
+ hud = com.rnattachmentkit.VideoExportHUD(act, snap.themeColor, snap.language).also { h ->
988
+ h.onCancel = { job.abort() }
1039
989
  h.show()
1040
990
  }
1041
991
  }
1042
992
  }
1043
- val videoFiles = mutableListOf<File>()
1044
-
993
+ val main = android.os.Handler(android.os.Looper.getMainLooper())
994
+ val emitter = { reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) }
1045
995
  try {
1046
- for (uri in uris) {
1047
- if (videoCancelled) throw com.rnattachmentkit.VideoExporter.CancelledException()
1048
- if (isVideoUri(uri)) {
1049
- val idx = currentIndex
1050
- val exporter = com.rnattachmentkit.VideoExporter(reactApplicationContext)
1051
- currentVideoExporter = exporter
1052
- val cancelledMap = {
1053
- WritableNativeMap().apply {
1054
- putString("originalUri", uri.toString())
1055
- putString("mediaType", "video")
1056
- putString("error", "CANCELLED")
1057
- putBoolean("cancelled", true)
1058
- }
1059
- }
1060
- val resultMap = try {
1061
- // 앱이 이미 취소한 동영상은 건너뜀
1062
- if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
1063
- job.begin(idx, exporter)
1064
- val state = lastVideoEditStates[uri.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
1065
- val throttle = com.rnattachmentkit.ProgressThrottle()
1066
- val onProgress: (Float) -> Unit = { p ->
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())
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 {
1070
1017
  putString("processingId", processingId)
1018
+ putInt("total", totalCount)
1019
+ putArray("items", arr)
1071
1020
  })
1072
1021
  }
1073
- // 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
1074
- // 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
1075
- val stop = { videoCancelled || job.isCancelled(idx) }
1076
- val res = com.rnattachmentkit.VideoPrepareManager.takeReady(uri, state, onProgress, stop)
1077
- ?: run {
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)
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())
1129
1027
  putString("processingId", processingId)
1130
1028
  })
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
1029
  }
1143
- } catch (e: Exception) {}
1144
1030
 
1145
- contentResolver.openInputStream(uri)?.use { inputStream ->
1146
- // Get original image dimensions
1147
- val originalOptions = BitmapFactory.Options().apply {
1148
- inJustDecodeBounds = true
1149
- }
1150
- BitmapFactory.decodeStream(inputStream, null, originalOptions)
1151
-
1152
- var originalWidth = originalOptions.outWidth
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
- }
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) {}
1282
1039
  }
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
1040
+ }).map { toWritableMap(it) }
1295
1041
  } finally {
1296
1042
  activeProcessingJobs.remove(processingId)
1297
1043
  if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
1298
1044
  }
1299
-
1300
- results
1301
1045
  }
1302
-
1303
- private fun calculateInSampleSize(
1304
- reqWidth: Int,
1305
- reqHeight: Int,
1306
- maxWidth: Int,
1307
- maxHeight: Int
1308
- ): Int {
1309
- var inSampleSize = 1
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
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())
1317
1055
  }
1318
1056
  }
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
1057
  }
1341
1058
 
1342
1059