Skip to content

Commit 90f2937

Browse files
authored
Merge pull request #126 from ismoy/develop
Add configurable camera preview scaling options
2 parents efe24f1 + bba6da4 commit 90f2937

23 files changed

Lines changed: 285 additions & 122 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Full-featured sample application showcasing:
8484
**Kotlin Multiplatform:**
8585
```kotlin
8686
dependencies {
87-
implementation("io.github.ismoy:imagepickerkmp:1.0.40")
87+
implementation("io.github.ismoy:imagepickerkmp:1.0.41")
8888
}
8989
```
9090

@@ -600,6 +600,13 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
600600
</a><br />
601601
<a href="https://github.com/ismoy/ImagePickerKMP/commits?author=YaminMahdi" title="Contributions">💻</a>
602602
</td>
603+
<td align="center" valign="top" width="14.28%">
604+
<a href="https://github.com/jadlr">
605+
<img src="https://avatars.githubusercontent.com/u/696999?v=4" width="100px;" alt="jadlr"/><br />
606+
<sub><b>jadlr</b></sub>
607+
</a><br />
608+
<a href="https://github.com/ismoy/ImagePickerKMP/commits?author=jadlr" title="Contributions">💻</a>
609+
</td>
603610
<td align="center" valign="top" width="14.28%">
604611
<a href="https://github.com/daniil-pastuhov">
605612
<img src="https://avatars.githubusercontent.com/u/8494442?v=4" width="100px;" alt="daniil-pastuhov"/><br />

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ plugins {
77
alias(libs.plugins.kover)
88
}
99

10-
version = "1.0.40"
10+
version = "1.0.41"

docs/API_REFERENCE.md

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -854,66 +854,92 @@ Configuration for camera capture.
854854

855855
```kotlin
856856
data class CameraCaptureConfig(
857-
val preference: CapturePhotoPreference = CapturePhotoPreference.QUALITY,
857+
val preference: CapturePhotoPreference = CapturePhotoPreference.BALANCED,
858858
val captureButtonSize: Dp = 72.dp,
859-
val compressionLevel: CompressionLevel? = null,
860-
val includeExif: Boolean = false,
859+
val compressionLevel: CompressionLevel? = CompressionLevel.MEDIUM,
860+
val includeExif: Boolean = false,
861+
val redactGpsData: Boolean = true,
861862
val uiConfig: UiConfig = UiConfig(),
862863
val cameraCallbacks: CameraCallbacks = CameraCallbacks(),
863864
val permissionAndConfirmationConfig: PermissionAndConfirmationConfig = PermissionAndConfirmationConfig(),
864-
val galleryConfig: GalleryConfig = GalleryConfig()
865+
val cropConfig: CropConfig = CropConfig(),
866+
val cameraScaleType: CameraScaleType = CameraScaleType.FILL_CENTER // NEW in v1.0.41
865867
)
866868
```
867869

868870
**Parameters:**
869871
- `preference` - Photo capture quality preference
870872
- `captureButtonSize` - Size of the capture button
871-
- `compressionLevel` - Automatic image compression level (null = disabled, MEDIUM = recommended)
872-
- `includeExif` - **NEW**: Extract EXIF metadata including GPS, camera model, timestamps (Android/iOS only)
873+
- `compressionLevel` - Automatic image compression level (`null` = disabled, `MEDIUM` = recommended)
874+
- `includeExif` - Extract EXIF metadata including GPS, camera model, timestamps (Android/iOS only)
875+
- `redactGpsData` - Strip GPS coordinates from EXIF before delivery (default `true`). Effective only when `includeExif = true`
873876
- `uiConfig` - UI customization configuration
874877
- `cameraCallbacks` - Camera lifecycle callbacks
875878
- `permissionAndConfirmationConfig` - Permission and confirmation dialogs
876-
- `galleryConfig` - Gallery selection configuration
879+
- `cropConfig` - Interactive crop UI configuration
880+
- `cameraScaleType` - <span class="pill-new">NEW in v1.0.41</span> How the camera preview is scaled inside its viewport (Android only). Defaults to `CameraScaleType.FILL_CENTER`
877881

878-
**Image Compression Examples:**
882+
**Camera scale type examples:**
879883

880884
```kotlin
885+
// Default — fills the viewport, crops the feed to fit
881886
CameraCaptureConfig()
882887

888+
// Letterbox — full camera feed visible, matches captured image framing
883889
CameraCaptureConfig(
884-
compressionLevel = CompressionLevel.MEDIUM
890+
cameraScaleType = CameraScaleType.FIT_CENTER
885891
)
886892

893+
// Fill from top-left
887894
CameraCaptureConfig(
888-
compressionLevel = CompressionLevel.HIGH
889-
)
890-
891-
CameraCaptureConfig(
892-
compressionLevel = CompressionLevel.LOW
895+
cameraScaleType = CameraScaleType.FILL_START
893896
)
894897
```
895898

896899
### PermissionAndConfirmationConfig
897900

898-
Configuration for permissions and confirmation.
901+
Configuration for permissions and post-capture confirmation screen.
899902

900903
```kotlin
901904
data class PermissionAndConfirmationConfig(
902905
val customPermissionHandler: ((PermissionConfig) -> Unit)? = null,
903-
val customConfirmationView: (@Composable (CameraPhotoHandler.PhotoResult, (CameraPhotoHandler.PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
904-
val customDeniedDialog: (@Composable ((onRetry: () -> Unit) -> Unit))? = null,
905-
val customSettingsDialog: (@Composable ((onOpenSettings: () -> Unit) -> Unit))? = null,
906-
val skipConfirmation: Boolean = false
906+
val customConfirmationView: (@Composable (PhotoResult, (PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
907+
val customDeniedDialog: (@Composable (onRetry: () -> Unit, onDismiss: () -> Unit) -> Unit)? = null,
908+
val customSettingsDialog: (@Composable (onOpenSettings: () -> Unit, onDismiss: () -> Unit) -> Unit)? = null,
909+
val skipConfirmation: Boolean = false,
910+
val cancelButtonTextIOS: String? = "Cancel",
911+
val onCancelPermissionConfigIOS: (() -> Unit)? = null,
912+
val confirmationImageContentScale: ContentScale = ContentScale.Crop // NEW in v1.0.41
907913
)
908914
```
909915

910916
#### Parameters
911917

912918
- `customPermissionHandler: ((PermissionConfig) -> Unit)?` - Custom permission handler for text-based customization
913919
- `customConfirmationView: (@Composable (...) -> Unit)?` - Custom composable for photo confirmation
914-
- `customDeniedDialog: (@Composable ((onRetry: () -> Unit) -> Unit))?` - Custom composable dialog when permission is denied
915-
- `customSettingsDialog: (@Composable ((onOpenSettings: () -> Unit) -> Unit))?` - Custom composable dialog for opening settings
916-
- `skipConfirmation: Boolean` - If true, automatically confirms the photo without showing confirmation screen (Android only)
920+
- `customDeniedDialog: (@Composable (...) -> Unit)?` - Custom composable dialog when permission is denied. Always call `onRetry` or `onDismiss` on user interaction
921+
- `customSettingsDialog: (@Composable (...) -> Unit)?` - Custom composable dialog for opening system settings. Always call `onOpenSettings` or `onDismiss` on user interaction
922+
- `skipConfirmation: Boolean` - If `true`, delivers the captured photo directly without showing the confirmation screen (Android)
923+
- `cancelButtonTextIOS: String?` - Text label for the cancel button in the iOS permission alert. Defaults to `"Cancel"`
924+
- `onCancelPermissionConfigIOS: (() -> Unit)?` - Callback invoked when the user taps cancel in the iOS permission alert
925+
- `confirmationImageContentScale: ContentScale` - <span class="pill-new">NEW in v1.0.41</span> How the captured photo is scaled in the post-capture confirmation preview. Accepts any Compose `ContentScale` value. Defaults to `ContentScale.Crop`
926+
927+
**Confirmation image scale examples:**
928+
929+
```kotlin
930+
// Default — crop to fill the preview area
931+
PermissionAndConfirmationConfig()
932+
933+
// Fit — show the entire photo letterboxed
934+
PermissionAndConfirmationConfig(
935+
confirmationImageContentScale = ContentScale.Fit
936+
)
937+
938+
// Fill width
939+
PermissionAndConfirmationConfig(
940+
confirmationImageContentScale = ContentScale.FillWidth
941+
)
942+
```
917943

918944
### UiConfig
919945

@@ -1298,6 +1324,29 @@ try {
12981324

12991325
## Enums
13001326

1327+
### CameraScaleType
1328+
1329+
> <span class="pill-new">NEW in v1.0.41</span>
1330+
1331+
Controls how the camera preview is scaled inside its viewport on Android. Mirrors `androidx.camera.view.PreviewView.ScaleType`.
1332+
1333+
```kotlin
1334+
enum class CameraScaleType {
1335+
FILL_CENTER, // Fill viewport, center-crop any overflow (default)
1336+
FILL_START, // Fill viewport, align to top-left, crop overflow
1337+
FILL_END, // Fill viewport, align to bottom-right, crop overflow
1338+
FIT_CENTER, // Letterbox — entire feed visible, centered
1339+
FIT_START, // Letterbox — entire feed visible, aligned to top-left
1340+
FIT_END // Letterbox — entire feed visible, aligned to bottom-right
1341+
}
1342+
```
1343+
1344+
**Notes:**
1345+
- `FILL_*` values crop the camera feed to fill the entire viewport. The visible viewfinder area may be narrower than the captured image.
1346+
- `FIT_*` values show the entire camera feed with letterboxing. The viewfinder framing matches the captured image exactly.
1347+
- Currently applied on Android only. iOS uses the system camera UI where preview and capture framing already match.
1348+
- Used in `CameraCaptureConfig.cameraScaleType`. Defaults to `FILL_CENTER`.
1349+
13011350
### CompressionLevel
13021351

13031352
Represents different compression levels for image processing.

docs/CHANGELOG.es.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ Todos los cambios notables en ImagePickerKMP serán documentados en este archivo
77
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
y este proyecto sigue [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [1.0.41] — 2026-05-05
11+
12+
### Agregado
13+
14+
- **Enum `CameraScaleType` — tipo de escala configurable para la vista previa de la cámara (Android)**
15+
- Nuevo enum `CameraScaleType`: `FILL_CENTER`, `FILL_START`, `FILL_END`, `FIT_CENTER`, `FIT_START`, `FIT_END`
16+
- Controla cómo se escala la vista previa de la cámara dentro de su viewport en Android
17+
- Los valores `FILL_*` rellenan el viewport completamente, recortando el feed de la cámara para que encaje
18+
- Los valores `FIT_*` muestran todo el feed con letterbox — el encuadre del visor coincide exactamente con la imagen capturada
19+
- Actualmente aplicado solo en Android; iOS usa la UI de cámara del sistema
20+
- Nuevo `CameraCaptureConfig.cameraScaleType: CameraScaleType` — por defecto `CameraScaleType.FILL_CENTER` (mantiene comportamiento anterior)
21+
22+
- **`PermissionAndConfirmationConfig.confirmationImageContentScale` — escala configurable en pantalla de confirmación**
23+
- Nuevo parámetro `confirmationImageContentScale: ContentScale` en `PermissionAndConfirmationConfig`
24+
- Controla cómo se escala la foto capturada en la pantalla de confirmación post-captura (Android)
25+
- Acepta cualquier valor de `ContentScale` de Compose: `Crop`, `Fit`, `FillWidth`, `FillHeight`, `FillBounds`, `Inside`, `None`
26+
- Por defecto `ContentScale.Crop` (mantiene comportamiento anterior)
27+
28+
### Cambiado
29+
30+
- Todos los comentarios en español dentro del código fuente traducidos al inglés
31+
1032
## [1.0.40] — 2026-04-29
1133

1234
### Agregado

docs/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to ImagePickerKMP will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.41] — 2026-05-05
9+
10+
### Added
11+
12+
- **`CameraScaleType` enum — configurable camera preview scale type (Android)**
13+
- New `CameraScaleType` enum: `FILL_CENTER`, `FILL_START`, `FILL_END`, `FIT_CENTER`, `FIT_START`, `FIT_END`
14+
- Controls how the camera preview is scaled inside its viewport on Android
15+
- `FILL_*` values fill the viewport entirely, cropping the camera feed to fit
16+
- `FIT_*` values letterbox the preview so the entire camera feed is visible — viewfinder framing matches the captured image exactly
17+
- Currently applied on Android only; iOS uses the system camera UI where preview and capture framing already match
18+
- New `CameraCaptureConfig.cameraScaleType: CameraScaleType` — defaults to `CameraScaleType.FILL_CENTER` (preserves previous behavior)
19+
20+
- **`PermissionAndConfirmationConfig.confirmationImageContentScale` — configurable post-capture confirmation image scale**
21+
- New `confirmationImageContentScale: ContentScale` parameter in `PermissionAndConfirmationConfig`
22+
- Controls how the captured photo is scaled in the post-capture confirmation preview screen (Android)
23+
- Accepts any Compose `ContentScale` value: `Crop`, `Fit`, `FillWidth`, `FillHeight`, `FillBounds`, `Inside`, `None`
24+
- Defaults to `ContentScale.Crop` (preserves previous behavior)
25+
26+
### Changed
27+
28+
- All Spanish-language inline comments across the codebase translated to English
29+
830
## [1.0.40] — 2026-04-29
931

1032
### Added

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ internal suspend fun applyCropUtils(
138138
)
139139
orientedBitmap.recycle()
140140

141-
// Aplicar máscara circular si se requiere, o usar directamente el bitmap recortado.
142-
// Se elimina createTransparentBitmap porque hacía una copia innecesaria que
143-
// podía degradar la calidad al reinterpretar el bitmap con fondo negro/transparente.
141+
// Apply circular mask if required, or use the cropped bitmap directly.
142+
// createTransparentBitmap was removed because it made an unnecessary copy that
143+
// could degrade quality by re-interpreting the bitmap with a black/transparent background.
144144
val finalBitmap = if (isCircularCrop) {
145145
val circular = createCircularBitmap(croppedBitmap)
146146
croppedBitmap.recycle()

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/presentation/ui/components/CameraCapturePreview.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ import androidx.camera.view.PreviewView
77
import androidx.compose.foundation.background
88
import androidx.compose.foundation.clickable
99
import androidx.compose.foundation.interaction.MutableInteractionSource
10-
import androidx.compose.foundation.isSystemInDarkTheme
1110
import androidx.compose.foundation.layout.Box
1211
import androidx.compose.foundation.layout.fillMaxSize
1312
import androidx.compose.foundation.layout.padding
1413
import androidx.compose.foundation.layout.size
1514
import androidx.compose.foundation.shape.CircleShape
1615
import androidx.compose.material.Icon
17-
import androidx.compose.material.MaterialTheme
1816
import androidx.compose.material.icons.Icons
1917
import androidx.compose.material.icons.filled.Cached
2018
import androidx.compose.runtime.Composable
@@ -32,11 +30,11 @@ import androidx.compose.ui.unit.dp
3230
import androidx.compose.ui.viewinterop.AndroidView
3331
import io.github.ismoy.imagepickerkmp.data.models.FlashMode
3432
import io.github.ismoy.imagepickerkmp.domain.config.CameraPreviewConfig
35-
import io.github.ismoy.imagepickerkmp.domain.config.ImagePickerUiConstants.BackgroundColor
3633
import io.github.ismoy.imagepickerkmp.domain.config.ImagePickerUiConstants.DELAY_TO_TAKE_PHOTO
3734
import io.github.ismoy.imagepickerkmp.domain.models.CapturePhotoPreference
3835
import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
3936
import io.github.ismoy.imagepickerkmp.domain.models.PhotoResult
37+
import io.github.ismoy.imagepickerkmp.domain.models.CameraScaleType
4038
import io.github.ismoy.imagepickerkmp.presentation.ui.extensions.activity
4139
import io.github.ismoy.imagepickerkmp.presentation.ui.utils.playShutterSound
4240
import io.github.ismoy.imagepickerkmp.presentation.ui.utils.rememberCameraManager
@@ -84,8 +82,6 @@ fun CameraCapturePreview(
8482
playShutterSound()
8583
stateHolder?.capturePhoto(onPhotoResult, onError, compressionLevel, includeExif, redactGpsData)
8684
}
87-
val isDark = isSystemInDarkTheme()
88-
val backgroundColor = if (isDark) MaterialTheme.colors.background else BackgroundColor
8985
val resolvedButtonColor = previewConfig.uiConfig.buttonColor ?: Color.Gray
9086
val resolvedIconColor = previewConfig.uiConfig.iconColor ?: Color.White
9187
val resolvedButtonSize = previewConfig.uiConfig.buttonSize ?: 56.dp
@@ -102,12 +98,12 @@ fun CameraCapturePreview(
10298
}
10399
}
104100

105-
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
101+
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
106102
AndroidView(
107103
factory = { context ->
108104
PreviewView(context).apply {
109-
scaleType = PreviewView.ScaleType.FILL_CENTER
110-
105+
scaleType = previewConfig.cameraScaleType.toPreviewViewScaleType()
106+
111107
implementationMode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
112108
PreviewView.ImplementationMode.COMPATIBLE
113109
} else {
@@ -183,3 +179,12 @@ fun CameraCapturePreview(
183179
FlashOverlay(visible = stateHolder?.showFlashOverlay ?: false)
184180
}
185181
}
182+
183+
private fun CameraScaleType.toPreviewViewScaleType(): PreviewView.ScaleType = when (this) {
184+
CameraScaleType.FILL_CENTER -> PreviewView.ScaleType.FILL_CENTER
185+
CameraScaleType.FILL_START -> PreviewView.ScaleType.FILL_START
186+
CameraScaleType.FILL_END -> PreviewView.ScaleType.FILL_END
187+
CameraScaleType.FIT_CENTER -> PreviewView.ScaleType.FIT_CENTER
188+
CameraScaleType.FIT_START -> PreviewView.ScaleType.FIT_START
189+
CameraScaleType.FIT_END -> PreviewView.ScaleType.FIT_END
190+
}

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/presentation/ui/components/ImageConfirmationViewWithCustomButtons.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ fun ImageConfirmationViewWithCustomButtons(
6060
onConfirm: (PhotoResult) -> Unit,
6161
onRetry: () -> Unit,
6262
customConfirmationView: (@Composable (PhotoResult, (PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
63-
uiConfig: UiConfig = UiConfig()
63+
uiConfig: UiConfig = UiConfig(),
64+
confirmationImageContentScale: ContentScale = ContentScale.Crop
6465
) {
6566
if (customConfirmationView != null) {
6667
customConfirmationView(result, onConfirm, onRetry)
@@ -90,7 +91,8 @@ fun ImageConfirmationViewWithCustomButtons(
9091
resolvedButtonColor = resolvedButtonColor,
9192
resolvedIconColor = resolvedIconColor,
9293
resolvedButtonSize = resolvedButtonSize,
93-
uiConfig = uiConfig
94+
uiConfig = uiConfig,
95+
confirmationImageContentScale = confirmationImageContentScale
9496
)
9597
} else {
9698
Card(
@@ -114,7 +116,7 @@ fun ImageConfirmationViewWithCustomButtons(
114116
.aspectRatio(ImagePickerUiConstants.ConfirmationCardImageAspectRatio)
115117
.clip(RoundedCornerShape(topStart = ImagePickerUiConstants.ConfirmationCardCornerRadius,
116118
topEnd = ImagePickerUiConstants.ConfirmationCardCornerRadius)),
117-
contentScale = ContentScale.Crop
119+
contentScale = confirmationImageContentScale
118120
)
119121
val isHD = (result.width ?: 0) >= 1280 && (result.height ?: 0) >= 720
120122
Column( modifier = Modifier

library/src/androidMain/kotlin/io/github/ismoy/imagepickerkmp/presentation/ui/components/LandscapeConfirmationLayout.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ internal fun LandscapeConfirmationLayout(
5555
resolvedButtonColor: Color,
5656
resolvedIconColor: Color,
5757
resolvedButtonSize: Dp,
58-
uiConfig: UiConfig
58+
uiConfig: UiConfig,
59+
confirmationImageContentScale: ContentScale = ContentScale.Crop
5960
) {
6061
Card(
6162
modifier = Modifier
@@ -83,7 +84,7 @@ internal fun LandscapeConfirmationLayout(
8384
topStart = ImagePickerUiConstants.ConfirmationCardCornerRadius,
8485
bottomStart = ImagePickerUiConstants.ConfirmationCardCornerRadius
8586
)),
86-
contentScale = ContentScale.Crop
87+
contentScale = confirmationImageContentScale
8788
)
8889

8990
val isHD = (result.width ?: 0) >= 1280 && (result.height ?: 0) >= 720

0 commit comments

Comments
 (0)