Skip to content

Commit 213b40d

Browse files
authored
Merge pull request #103 from ismoy/develop
Develop
2 parents a43dea8 + 222bb53 commit 213b40d

23 files changed

Lines changed: 290 additions & 129 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ plugins {
77
id("jacoco")
88
}
99

10-
version = "1.0.33"
10+
version = "1.0.34"

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ cameraCore = "1.5.1"
55
cameraView = "1.5.1"
66
core = "3.5.3"
77
exifinterface = "1.4.2"
8+
lifecycleRuntimeCompose = "2.9.0"
89
kotlin = "2.1.21"
910
android-minSdk = "24"
1011
android-compileSdk = "36"
@@ -23,6 +24,7 @@ androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "
2324
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
2425
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraView" }
2526
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
27+
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
2628
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtendedVersion" }
2729
androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "ui" }
2830
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "ui" }

library/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ kotlin {
396396
val androidMain by getting {
397397
dependencies {
398398
implementation(libs.androidx.activity.compose)
399+
implementation(libs.androidx.lifecycle.runtime.compose)
399400
implementation(libs.androidx.camera.core)
400401
implementation(libs.androidx.camera.camera2)
401402
implementation(libs.androidx.camera.lifecycle)

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/camera/CameraController.kt

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package io.github.ismoy.imagepickerkmp.data.camera
22

33
import android.content.Context
44
import android.os.Build
5+
import android.util.Size
56
import androidx.camera.core.AspectRatio
67
import androidx.camera.core.CameraSelector
78
import androidx.camera.core.ImageCapture
89
import androidx.camera.core.ImageCaptureException
910
import androidx.camera.core.Preview
11+
import androidx.camera.core.resolutionselector.AspectRatioStrategy
12+
import androidx.camera.core.resolutionselector.ResolutionSelector
13+
import androidx.camera.core.resolutionselector.ResolutionStrategy
1014
import androidx.camera.lifecycle.ProcessCameraProvider
15+
import androidx.camera.lifecycle.awaitInstance
1116
import androidx.camera.view.PreviewView
1217
import androidx.core.content.ContextCompat
1318
import androidx.lifecycle.LifecycleOwner
@@ -46,31 +51,48 @@ internal class CameraController(
4651
preference: CapturePhotoPreference
4752
) {
4853
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
49-
delay(100)
54+
delay(50)
5055
}
51-
52-
cameraProvider = withContext(Dispatchers.IO) {
53-
ProcessCameraProvider.getInstance(context).get()
56+
57+
cameraProvider = withContext(Dispatchers.Main) {
58+
ProcessCameraProvider.awaitInstance(context)
5459
}
5560

5661
withContext(Dispatchers.Main) {
5762
if (HighPerformanceConfig.requiresCompatibilityMode()) {
5863
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
5964
}
60-
61-
val preview = Preview.Builder()
62-
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
63-
.build()
64-
.also {
65-
it.surfaceProvider = previewView.surfaceProvider
66-
}
6765

68-
imageCapture = ImageCapture.Builder()
66+
val resolutionSelector =
67+
ResolutionSelector.Builder()
68+
.setAspectRatioStrategy(
69+
AspectRatioStrategy(
70+
AspectRatio.RATIO_4_3,
71+
AspectRatioStrategy.FALLBACK_RULE_AUTO
72+
)
73+
)
74+
.setResolutionStrategy(
75+
ResolutionStrategy(
76+
Size(4000, 3000),
77+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
78+
)
79+
)
80+
.build()
81+
82+
val previewBuilder = Preview.Builder()
83+
previewBuilder.setResolutionSelector(resolutionSelector)
84+
val preview = previewBuilder.build().also {
85+
it.surfaceProvider = previewView.surfaceProvider
86+
}
87+
88+
val imageCaptureBuilder = ImageCapture.Builder()
6989
.setCaptureMode(getCaptureModeFn(preference))
7090
.setFlashMode(getImageCaptureFlashMode(currentFlashMode))
71-
.setJpegQuality(HighPerformanceConfig.getOptimalJpegQuality())
72-
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
73-
.build()
91+
.setJpegQuality(HighPerformanceConfig.getOptimalJpegQuality(context))
92+
93+
imageCaptureBuilder.setResolutionSelector(resolutionSelector)
94+
95+
imageCapture = imageCaptureBuilder.build()
7496

7597
val cameraSelector = when (currentCameraType) {
7698
CameraType.BACK -> CameraSelector.DEFAULT_BACK_CAMERA
@@ -138,5 +160,6 @@ internal class CameraController(
138160

139161
fun stopCamera() {
140162
cameraProvider?.unbindAll()
163+
imageCapture = null
141164
}
142165
}

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/camera/CameraXManager.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
55
import io.github.ismoy.imagepickerkmp.domain.models.CapturePhotoPreference
66
import io.github.ismoy.imagepickerkmp.domain.models.PhotoResult
77

8-
/**
9-
* High-level manager for camera operations, providing an interface to start, stop, and control the camera.
10-
*/
8+
119
internal class CameraXManager(
1210
private val cameraController: CameraController,
1311
private val imageProcessor: io.github.ismoy.imagepickerkmp.data.processors.ImageProcessor

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/managers/FileManager.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ import java.text.SimpleDateFormat
88
import java.util.Date
99
import java.util.Locale
1010

11-
/**
12-
* Utility class for managing file operations related to image capture and storage.
13-
*
14-
* Provides methods to create image files and convert files to URI strings.
15-
*/
11+
1612
internal class FileManager(private val context: Context) {
1713

1814
fun createImageFile(): File {
1915
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
2016
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
17+
?: context.filesDir
18+
if (!storageDir.exists()) storageDir.mkdirs()
2119
return File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir)
2220
}
2321

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/processors/ImageOrientationCorrector.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ import io.github.ismoy.imagepickerkmp.domain.config.ImagePickerUiConstants.ORIEN
1818
import java.io.File
1919
import java.io.FileOutputStream
2020

21-
/**
22-
* Handles image orientation correction operations.
23-
*
24-
* SOLID: Single Responsibility - Only handles orientation correction
25-
* SOLID: Open/Closed - Can be extended for different correction algorithms
26-
*/
21+
2722
internal class ImageOrientationCorrector {
2823

2924
@SuppressLint("ExifInterface")
@@ -66,9 +61,12 @@ internal class ImageOrientationCorrector {
6661
val outputFile = if (matrix.isIdentity) {
6762
imageFile
6863
} else {
69-
val correctedFile = File(imageFile.parentFile, "corrected_${imageFile.name}")
64+
val parentDir = imageFile.parentFile ?: imageFile.canonicalFile.parentFile
65+
?: throw IllegalStateException("Cannot resolve parent directory for: ${imageFile.absolutePath}")
66+
val correctedFile = File(parentDir, "corrected_${imageFile.name}")
67+
val recompressQuality = HighPerformanceConfig.getRecompressQuality()
7068
FileOutputStream(correctedFile).use { out ->
71-
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 95, out)
69+
finalBitmap.compress(Bitmap.CompressFormat.JPEG, recompressQuality, out)
7270
}
7371
finalBitmap.recycle()
7472
correctedFile

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/processors/ImageProcessor.kt

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.content.Context
44
import android.graphics.Bitmap
55
import android.graphics.BitmapFactory
66
import android.net.Uri
7-
import androidx.core.content.ContextCompat
87
import io.github.ismoy.imagepickerkmp.data.camera.CameraController.CameraType
98
import io.github.ismoy.imagepickerkmp.domain.exceptions.ImageProcessingException
109
import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
@@ -13,17 +12,18 @@ import io.github.ismoy.imagepickerkmp.domain.config.HighPerformanceConfig
1312
import io.github.ismoy.imagepickerkmp.domain.utils.ExifDataExtractor
1413
import kotlinx.coroutines.CoroutineScope
1514
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.SupervisorJob
1616
import kotlinx.coroutines.launch
1717
import kotlinx.coroutines.withContext
1818
import java.io.File
19-
import java.io.FileOutputStream
2019
import androidx.core.graphics.scale
2120

2221

2322
internal class ImageProcessor(
2423
private val context: Context,
2524
private val fileManager: io.github.ismoy.imagepickerkmp.data.managers.FileManager,
26-
private val orientationCorrector: ImageOrientationCorrector
25+
private val orientationCorrector: ImageOrientationCorrector,
26+
private val processingScope: CoroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
2727
) {
2828

2929
fun processImage(
@@ -34,7 +34,7 @@ internal class ImageProcessor(
3434
onPhotoCaptured: (PhotoResult) -> Unit,
3535
onError: (Exception) -> Unit
3636
) {
37-
CoroutineScope(Dispatchers.Default).launch {
37+
processingScope.launch {
3838
try {
3939
val exifData = if (includeExif) {
4040
try {
@@ -51,7 +51,7 @@ internal class ImageProcessor(
5151
val correctedImageFile = orientationCorrector.correctImageOrientation(imageFile, cameraType)
5252

5353
val options = BitmapFactory.Options().apply {
54-
inPreferredConfig = if (HighPerformanceConfig.isHighEndDevice()) {
54+
inPreferredConfig = if (HighPerformanceConfig.isHighEndDevice(context)) {
5555
Bitmap.Config.ARGB_8888
5656
} else {
5757
Bitmap.Config.RGB_565
@@ -61,9 +61,16 @@ internal class ImageProcessor(
6161
}
6262

6363
val originalBitmap = BitmapFactory.decodeFile(correctedImageFile.absolutePath, options)
64+
?: run {
65+
withContext(Dispatchers.Main) {
66+
onError(ImageProcessingException("Failed to decode captured image."))
67+
}
68+
return@launch
69+
}
6470

65-
if (originalBitmap != null) {
66-
val processedBitmap = if (compressionLevel != null) {
71+
var processedBitmap: Bitmap? = null
72+
try {
73+
processedBitmap = if (compressionLevel != null) {
6774
processImageWithCompression(originalBitmap, compressionLevel)
6875
} else {
6976
originalBitmap
@@ -88,14 +95,23 @@ internal class ImageProcessor(
8895
originalBitmap.recycle()
8996
}
9097
processedBitmap.recycle()
98+
processedBitmap = null
99+
if (compressionLevel != null && finalFile != correctedImageFile) {
100+
correctedImageFile.delete()
101+
}
102+
if (correctedImageFile != imageFile) {
103+
imageFile.delete()
104+
}
91105

92106
withContext(Dispatchers.Main) {
93107
onPhotoCaptured(result)
94108
}
95-
} else {
96-
withContext(Dispatchers.Main) {
97-
onError(ImageProcessingException("Failed to decode captured image."))
109+
} catch (e: Exception) {
110+
if (processedBitmap != null && processedBitmap != originalBitmap) {
111+
processedBitmap.recycle()
98112
}
113+
if (!originalBitmap.isRecycled) originalBitmap.recycle()
114+
throw e
99115
}
100116
} catch (e: Exception) {
101117
withContext(Dispatchers.Main) {

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/processors/ProcessImageWithCompression.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import android.graphics.Bitmap
44
import androidx.core.graphics.scale
55
import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
66

7-
internal fun processImageWithCompression(
7+
8+
internal fun processImageWithCompression(
89
bitmap: Bitmap,
910
compressionLevel: CompressionLevel
1011
): Bitmap {
12+
if (compressionLevel == CompressionLevel.LOW) return bitmap
13+
1114
val maxDimension = when (compressionLevel) {
12-
CompressionLevel.HIGH -> 1280
13-
CompressionLevel.MEDIUM -> 1920
14-
CompressionLevel.LOW -> 2560
15+
CompressionLevel.HIGH -> 1920
16+
CompressionLevel.MEDIUM -> 3840
17+
CompressionLevel.LOW -> Int.MAX_VALUE
1518
}
1619

1720
val currentMaxDimension = maxOf(bitmap.width, bitmap.height)
@@ -21,7 +24,7 @@ import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
2124
val targetWidth = (bitmap.width * scale).toInt()
2225
val targetHeight = (bitmap.height * scale).toInt()
2326

24-
val resizedBitmap = bitmap.scale(targetWidth, targetHeight, false)
27+
val resizedBitmap = bitmap.scale(targetWidth, targetHeight, true)
2528
if (resizedBitmap != bitmap) {
2629
bitmap.recycle()
2730
}

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/data/processors/SaveCompressedImage.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import java.io.FileOutputStream
1010
originalFile: File,
1111
compressionLevel: CompressionLevel
1212
): File {
13-
val outputFile = File(originalFile.parentFile, "compressed_${originalFile.name}")
13+
val parentDir = originalFile.parentFile ?: originalFile.canonicalFile.parentFile
14+
?: throw IllegalStateException("Cannot resolve parent directory for: ${originalFile.absolutePath}")
15+
val outputFile = File(parentDir, "compressed_${originalFile.name}")
1416
val quality = (compressionLevel.toQualityValue() * 100).toInt()
1517

1618
FileOutputStream(outputFile).use { outputStream ->

0 commit comments

Comments
 (0)