Skip to content
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Full-featured sample application showcasing:
**Kotlin Multiplatform:**
```kotlin
dependencies {
implementation("io.github.ismoy:imagepickerkmp:1.0.40")
implementation("io.github.ismoy:imagepickerkmp:1.0.41")
}
```

Expand Down Expand Up @@ -600,6 +600,13 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
</a><br />
<a href="https://github.com/ismoy/ImagePickerKMP/commits?author=YaminMahdi" title="Contributions">💻</a>
</td>
<td align="center" valign="top" width="14.28%">
<a href="https://github.com/jadlr">
<img src="https://avatars.githubusercontent.com/u/696999?v=4" width="100px;" alt="jadlr"/><br />
<sub><b>jadlr</b></sub>
</a><br />
<a href="https://github.com/ismoy/ImagePickerKMP/commits?author=jadlr" title="Contributions">💻</a>
</td>
<td align="center" valign="top" width="14.28%">
<a href="https://github.com/daniil-pastuhov">
<img src="https://avatars.githubusercontent.com/u/8494442?v=4" width="100px;" alt="daniil-pastuhov"/><br />
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ plugins {
alias(libs.plugins.kover)
}

version = "1.0.40"
version = "1.0.41"
93 changes: 71 additions & 22 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,66 +854,92 @@ Configuration for camera capture.

```kotlin
data class CameraCaptureConfig(
val preference: CapturePhotoPreference = CapturePhotoPreference.QUALITY,
val preference: CapturePhotoPreference = CapturePhotoPreference.BALANCED,
val captureButtonSize: Dp = 72.dp,
val compressionLevel: CompressionLevel? = null,
val includeExif: Boolean = false,
val compressionLevel: CompressionLevel? = CompressionLevel.MEDIUM,
val includeExif: Boolean = false,
val redactGpsData: Boolean = true,
val uiConfig: UiConfig = UiConfig(),
val cameraCallbacks: CameraCallbacks = CameraCallbacks(),
val permissionAndConfirmationConfig: PermissionAndConfirmationConfig = PermissionAndConfirmationConfig(),
val galleryConfig: GalleryConfig = GalleryConfig()
val cropConfig: CropConfig = CropConfig(),
val cameraScaleType: CameraScaleType = CameraScaleType.FILL_CENTER // NEW in v1.0.41
)
```

**Parameters:**
- `preference` - Photo capture quality preference
- `captureButtonSize` - Size of the capture button
- `compressionLevel` - Automatic image compression level (null = disabled, MEDIUM = recommended)
- `includeExif` - **NEW**: Extract EXIF metadata including GPS, camera model, timestamps (Android/iOS only)
- `compressionLevel` - Automatic image compression level (`null` = disabled, `MEDIUM` = recommended)
- `includeExif` - Extract EXIF metadata including GPS, camera model, timestamps (Android/iOS only)
- `redactGpsData` - Strip GPS coordinates from EXIF before delivery (default `true`). Effective only when `includeExif = true`
- `uiConfig` - UI customization configuration
- `cameraCallbacks` - Camera lifecycle callbacks
- `permissionAndConfirmationConfig` - Permission and confirmation dialogs
- `galleryConfig` - Gallery selection configuration
- `cropConfig` - Interactive crop UI configuration
- `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`

**Image Compression Examples:**
**Camera scale type examples:**

```kotlin
// Default — fills the viewport, crops the feed to fit
CameraCaptureConfig()

// Letterbox — full camera feed visible, matches captured image framing
CameraCaptureConfig(
compressionLevel = CompressionLevel.MEDIUM
cameraScaleType = CameraScaleType.FIT_CENTER
)

// Fill from top-left
CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH
)

CameraCaptureConfig(
compressionLevel = CompressionLevel.LOW
cameraScaleType = CameraScaleType.FILL_START
)
```

### PermissionAndConfirmationConfig

Configuration for permissions and confirmation.
Configuration for permissions and post-capture confirmation screen.

```kotlin
data class PermissionAndConfirmationConfig(
val customPermissionHandler: ((PermissionConfig) -> Unit)? = null,
val customConfirmationView: (@Composable (CameraPhotoHandler.PhotoResult, (CameraPhotoHandler.PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
val customDeniedDialog: (@Composable ((onRetry: () -> Unit) -> Unit))? = null,
val customSettingsDialog: (@Composable ((onOpenSettings: () -> Unit) -> Unit))? = null,
val skipConfirmation: Boolean = false
val customConfirmationView: (@Composable (PhotoResult, (PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
val customDeniedDialog: (@Composable (onRetry: () -> Unit, onDismiss: () -> Unit) -> Unit)? = null,
val customSettingsDialog: (@Composable (onOpenSettings: () -> Unit, onDismiss: () -> Unit) -> Unit)? = null,
val skipConfirmation: Boolean = false,
val cancelButtonTextIOS: String? = "Cancel",
val onCancelPermissionConfigIOS: (() -> Unit)? = null,
val confirmationImageContentScale: ContentScale = ContentScale.Crop // NEW in v1.0.41
)
```

#### Parameters

- `customPermissionHandler: ((PermissionConfig) -> Unit)?` - Custom permission handler for text-based customization
- `customConfirmationView: (@Composable (...) -> Unit)?` - Custom composable for photo confirmation
- `customDeniedDialog: (@Composable ((onRetry: () -> Unit) -> Unit))?` - Custom composable dialog when permission is denied
- `customSettingsDialog: (@Composable ((onOpenSettings: () -> Unit) -> Unit))?` - Custom composable dialog for opening settings
- `skipConfirmation: Boolean` - If true, automatically confirms the photo without showing confirmation screen (Android only)
- `customDeniedDialog: (@Composable (...) -> Unit)?` - Custom composable dialog when permission is denied. Always call `onRetry` or `onDismiss` on user interaction
- `customSettingsDialog: (@Composable (...) -> Unit)?` - Custom composable dialog for opening system settings. Always call `onOpenSettings` or `onDismiss` on user interaction
- `skipConfirmation: Boolean` - If `true`, delivers the captured photo directly without showing the confirmation screen (Android)
- `cancelButtonTextIOS: String?` - Text label for the cancel button in the iOS permission alert. Defaults to `"Cancel"`
- `onCancelPermissionConfigIOS: (() -> Unit)?` - Callback invoked when the user taps cancel in the iOS permission alert
- `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`

**Confirmation image scale examples:**

```kotlin
// Default — crop to fill the preview area
PermissionAndConfirmationConfig()

// Fit — show the entire photo letterboxed
PermissionAndConfirmationConfig(
confirmationImageContentScale = ContentScale.Fit
)

// Fill width
PermissionAndConfirmationConfig(
confirmationImageContentScale = ContentScale.FillWidth
)
```

### UiConfig

Expand Down Expand Up @@ -1298,6 +1324,29 @@ try {

## Enums

### CameraScaleType

> <span class="pill-new">NEW in v1.0.41</span>

Controls how the camera preview is scaled inside its viewport on Android. Mirrors `androidx.camera.view.PreviewView.ScaleType`.

```kotlin
enum class CameraScaleType {
FILL_CENTER, // Fill viewport, center-crop any overflow (default)
FILL_START, // Fill viewport, align to top-left, crop overflow
FILL_END, // Fill viewport, align to bottom-right, crop overflow
FIT_CENTER, // Letterbox — entire feed visible, centered
FIT_START, // Letterbox — entire feed visible, aligned to top-left
FIT_END // Letterbox — entire feed visible, aligned to bottom-right
}
```

**Notes:**
- `FILL_*` values crop the camera feed to fill the entire viewport. The visible viewfinder area may be narrower than the captured image.
- `FIT_*` values show the entire camera feed with letterboxing. The viewfinder framing matches the captured image exactly.
- Currently applied on Android only. iOS uses the system camera UI where preview and capture framing already match.
- Used in `CameraCaptureConfig.cameraScaleType`. Defaults to `FILL_CENTER`.

### CompressionLevel

Represents different compression levels for image processing.
Expand Down
22 changes: 22 additions & 0 deletions docs/CHANGELOG.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ Todos los cambios notables en ImagePickerKMP serán documentados en este archivo
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
y este proyecto sigue [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.41] — 2026-05-05

### Agregado

- **Enum `CameraScaleType` — tipo de escala configurable para la vista previa de la cámara (Android)**
- Nuevo enum `CameraScaleType`: `FILL_CENTER`, `FILL_START`, `FILL_END`, `FIT_CENTER`, `FIT_START`, `FIT_END`
- Controla cómo se escala la vista previa de la cámara dentro de su viewport en Android
- Los valores `FILL_*` rellenan el viewport completamente, recortando el feed de la cámara para que encaje
- Los valores `FIT_*` muestran todo el feed con letterbox — el encuadre del visor coincide exactamente con la imagen capturada
- Actualmente aplicado solo en Android; iOS usa la UI de cámara del sistema
- Nuevo `CameraCaptureConfig.cameraScaleType: CameraScaleType` — por defecto `CameraScaleType.FILL_CENTER` (mantiene comportamiento anterior)

- **`PermissionAndConfirmationConfig.confirmationImageContentScale` — escala configurable en pantalla de confirmación**
- Nuevo parámetro `confirmationImageContentScale: ContentScale` en `PermissionAndConfirmationConfig`
- Controla cómo se escala la foto capturada en la pantalla de confirmación post-captura (Android)
- Acepta cualquier valor de `ContentScale` de Compose: `Crop`, `Fit`, `FillWidth`, `FillHeight`, `FillBounds`, `Inside`, `None`
- Por defecto `ContentScale.Crop` (mantiene comportamiento anterior)

### Cambiado

- Todos los comentarios en español dentro del código fuente traducidos al inglés

## [1.0.40] — 2026-04-29

### Agregado
Expand Down
22 changes: 22 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to ImagePickerKMP will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.41] — 2026-05-05

### Added

- **`CameraScaleType` enum — configurable camera preview scale type (Android)**
- New `CameraScaleType` enum: `FILL_CENTER`, `FILL_START`, `FILL_END`, `FIT_CENTER`, `FIT_START`, `FIT_END`
- Controls how the camera preview is scaled inside its viewport on Android
- `FILL_*` values fill the viewport entirely, cropping the camera feed to fit
- `FIT_*` values letterbox the preview so the entire camera feed is visible — viewfinder framing matches the captured image exactly
- Currently applied on Android only; iOS uses the system camera UI where preview and capture framing already match
- New `CameraCaptureConfig.cameraScaleType: CameraScaleType` — defaults to `CameraScaleType.FILL_CENTER` (preserves previous behavior)

- **`PermissionAndConfirmationConfig.confirmationImageContentScale` — configurable post-capture confirmation image scale**
- New `confirmationImageContentScale: ContentScale` parameter in `PermissionAndConfirmationConfig`
- Controls how the captured photo is scaled in the post-capture confirmation preview screen (Android)
- Accepts any Compose `ContentScale` value: `Crop`, `Fit`, `FillWidth`, `FillHeight`, `FillBounds`, `Inside`, `None`
- Defaults to `ContentScale.Crop` (preserves previous behavior)

### Changed

- All Spanish-language inline comments across the codebase translated to English

## [1.0.40] — 2026-04-29

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ internal suspend fun applyCropUtils(
)
orientedBitmap.recycle()

// Aplicar máscara circular si se requiere, o usar directamente el bitmap recortado.
// Se elimina createTransparentBitmap porque hacía una copia innecesaria que
// podía degradar la calidad al reinterpretar el bitmap con fondo negro/transparente.
// Apply circular mask if required, or use the cropped bitmap directly.
// createTransparentBitmap was removed because it made an unnecessary copy that
// could degrade quality by re-interpreting the bitmap with a black/transparent background.
val finalBitmap = if (isCircularCrop) {
val circular = createCircularBitmap(croppedBitmap)
croppedBitmap.recycle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cached
import androidx.compose.runtime.Composable
Expand All @@ -32,11 +30,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import io.github.ismoy.imagepickerkmp.data.models.FlashMode
import io.github.ismoy.imagepickerkmp.domain.config.CameraPreviewConfig
import io.github.ismoy.imagepickerkmp.domain.config.ImagePickerUiConstants.BackgroundColor
import io.github.ismoy.imagepickerkmp.domain.config.ImagePickerUiConstants.DELAY_TO_TAKE_PHOTO
import io.github.ismoy.imagepickerkmp.domain.models.CapturePhotoPreference
import io.github.ismoy.imagepickerkmp.domain.models.CompressionLevel
import io.github.ismoy.imagepickerkmp.domain.models.PhotoResult
import io.github.ismoy.imagepickerkmp.domain.models.CameraScaleType
import io.github.ismoy.imagepickerkmp.presentation.ui.extensions.activity
import io.github.ismoy.imagepickerkmp.presentation.ui.utils.playShutterSound
import io.github.ismoy.imagepickerkmp.presentation.ui.utils.rememberCameraManager
Expand Down Expand Up @@ -84,8 +82,6 @@ fun CameraCapturePreview(
playShutterSound()
stateHolder?.capturePhoto(onPhotoResult, onError, compressionLevel, includeExif, redactGpsData)
}
val isDark = isSystemInDarkTheme()
val backgroundColor = if (isDark) MaterialTheme.colors.background else BackgroundColor
val resolvedButtonColor = previewConfig.uiConfig.buttonColor ?: Color.Gray
val resolvedIconColor = previewConfig.uiConfig.iconColor ?: Color.White
val resolvedButtonSize = previewConfig.uiConfig.buttonSize ?: 56.dp
Expand All @@ -102,12 +98,12 @@ fun CameraCapturePreview(
}
}

Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
AndroidView(
factory = { context ->
PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
scaleType = previewConfig.cameraScaleType.toPreviewViewScaleType()

implementationMode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
PreviewView.ImplementationMode.COMPATIBLE
} else {
Expand Down Expand Up @@ -183,3 +179,12 @@ fun CameraCapturePreview(
FlashOverlay(visible = stateHolder?.showFlashOverlay ?: false)
}
}

private fun CameraScaleType.toPreviewViewScaleType(): PreviewView.ScaleType = when (this) {
CameraScaleType.FILL_CENTER -> PreviewView.ScaleType.FILL_CENTER
CameraScaleType.FILL_START -> PreviewView.ScaleType.FILL_START
CameraScaleType.FILL_END -> PreviewView.ScaleType.FILL_END
CameraScaleType.FIT_CENTER -> PreviewView.ScaleType.FIT_CENTER
CameraScaleType.FIT_START -> PreviewView.ScaleType.FIT_START
CameraScaleType.FIT_END -> PreviewView.ScaleType.FIT_END
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ fun ImageConfirmationViewWithCustomButtons(
onConfirm: (PhotoResult) -> Unit,
onRetry: () -> Unit,
customConfirmationView: (@Composable (PhotoResult, (PhotoResult) -> Unit, () -> Unit) -> Unit)? = null,
uiConfig: UiConfig = UiConfig()
uiConfig: UiConfig = UiConfig(),
confirmationImageContentScale: ContentScale = ContentScale.Crop
) {
if (customConfirmationView != null) {
customConfirmationView(result, onConfirm, onRetry)
Expand Down Expand Up @@ -90,7 +91,8 @@ fun ImageConfirmationViewWithCustomButtons(
resolvedButtonColor = resolvedButtonColor,
resolvedIconColor = resolvedIconColor,
resolvedButtonSize = resolvedButtonSize,
uiConfig = uiConfig
uiConfig = uiConfig,
confirmationImageContentScale = confirmationImageContentScale
)
} else {
Card(
Expand All @@ -114,7 +116,7 @@ fun ImageConfirmationViewWithCustomButtons(
.aspectRatio(ImagePickerUiConstants.ConfirmationCardImageAspectRatio)
.clip(RoundedCornerShape(topStart = ImagePickerUiConstants.ConfirmationCardCornerRadius,
topEnd = ImagePickerUiConstants.ConfirmationCardCornerRadius)),
contentScale = ContentScale.Crop
contentScale = confirmationImageContentScale
)
val isHD = (result.width ?: 0) >= 1280 && (result.height ?: 0) >= 720
Column( modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ internal fun LandscapeConfirmationLayout(
resolvedButtonColor: Color,
resolvedIconColor: Color,
resolvedButtonSize: Dp,
uiConfig: UiConfig
uiConfig: UiConfig,
confirmationImageContentScale: ContentScale = ContentScale.Crop
) {
Card(
modifier = Modifier
Expand Down Expand Up @@ -83,7 +84,7 @@ internal fun LandscapeConfirmationLayout(
topStart = ImagePickerUiConstants.ConfirmationCardCornerRadius,
bottomStart = ImagePickerUiConstants.ConfirmationCardCornerRadius
)),
contentScale = ContentScale.Crop
contentScale = confirmationImageContentScale
)

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