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.
- package/CHANGELOG.md +10 -0
- 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 +58 -341
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +58 -341
- package/ios/RNAttachmentKit.swift +64 -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 +6419 -422
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +107 -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 +107 -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 +6419 -422
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +107 -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 +107 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6419 -422
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +107 -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 +107 -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
|
@@ -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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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 &&
|
|
984
|
+
if (hasVideo && snap.showHUD) {
|
|
1032
985
|
withContext(Dispatchers.Main) {
|
|
1033
986
|
val act = getCurrentActivity() ?: return@withContext
|
|
1034
|
-
hud = com.rnattachmentkit.VideoExportHUD(act,
|
|
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
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
|