ImagePickerKMP – Kotlin Multiplatform Camera & Gallery Picker
The most complete camera & gallery picker library for Kotlin Multiplatform. One unified API for Android, iOS, Desktop, Web and WASM.
rememberImagePickerKMP NEW
The recommended Compose API. One call returns ImagePickerKMPState — reactive result, launchCamera() & launchGallery(). Zero booleans. Explore →
Camera Capture
Native camera with flash, rotation, zoom, crop and compression on Android & iOS.
Gallery Picker
Single & multiple selection, MIME filtering, selection limit, EXIF extraction.
Crop & Edit
Free, square and circular crop with zoom, rotation and aspect ratio lock.
EXIF Metadata
GPS, altitude, camera model, ISO, aperture, focal length — Android & iOS.
Compression
LOW / MEDIUM / HIGH quality levels with async processing and bitmap recycling.
Format Support
JPEG, PNG, HEIC, HEIF, WebP, GIF, BMP across all platforms.
UI Customization
Custom button colors, icons, permission dialogs, confirmation screens and camera callbacks.
Requirements
| Requirement | Minimum Version | Notes |
|---|---|---|
| Kotlin | 2.3.20 | Breaking — ABI incompatible with < 2.3.x |
| Compose Multiplatform | 1.10.3 | Requires Kotlin 2.3.x |
| Android minSdk | 24 | |
| Android compileSdk | 36 | |
| iOS | 12.0+ | |
| JDK (Desktop) | 21+ | |
| imagepicker-core | 0.0.1 | Transitive — included automatically via any module |
ABI version incompatible. If you need Kotlin 2.1.x, use a previous release.Installation
Kotlin Multiplatform
// commonMain dependencies — add only what your app uses
// ✅ Available modules
implementation("io.github.ismoy:imagepickerkmp:1.1.7") // Photo
implementation("io.github.ismoy:imagepickerkmp-scanner:1.0.0") // Scanner — Android / iOS
// 🔜 Coming soon — not yet published
// implementation("io.github.ismoy:imagepickerkmp-video:??")
// implementation("io.github.ismoy:imagepickerkmp-audio:??")
// implementation("io.github.ismoy:imagepickerkmp-audio-player:??")
// implementation("io.github.ismoy:imagepickerkmp-video-player:??")
React / JavaScript (NPM)
npm install imagepickerkmp
imagepickerkmp, imagepicker-core, and imagepickerkmp-scanner are available now. Video, audio, and player modules remain previews and will be published as separate artifacts when released.Permissions Setup
<!-- Info.plist -->
<key>NSCameraUsageDescription</key>
<string>Camera access to capture photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access to select images</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save captured photos to your library</string>
<!-- Required when includeExif = true -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location for photo geotagging</string>
<!-- AndroidManifest.xml -->
<!-- No permissions required — the library manages them automatically -->
<!-- Optional: declare CAMERA only if your app directly uses the camera -->
<uses-permission
android:name="android.permission.CAMERA"
android:required="false" />
CAMERA permission is optional — add it only if your app directly accesses the camera outside of this library.Internationalization (i18n) 14 Languages
ImagePickerKMP delivers fully automated, multi-language internationalization powered by the i18nKonfig Gradle plugin created by Ismoy Belizaire. All user-facing strings — permission dialogs, confirmation prompts, crop controls, and camera UI — automatically adapt to the user's device language across Android, iOS, Desktop JVM, JS/Web, and WASM.
photo, video, audio, scanner) packages only its own localized string keys. Your app only downloads and bundles translations for the features you actually use.Supported Languages (Out-of-the-box)
14 worldwide languages are built-in and ready for instant use:
How It Works
When you initialize any picker via rememberImagePickerKMP(), rememberVideoPicker(), rememberAudioPicker(), or rememberScannerPicker(), the library automatically calls getLanguageDevice() to detect the system locale and configure strings for you.
// 1. Automatic Zero-Config (Device language is detected automatically)
val picker = rememberImagePickerKMP()
// 2. Optional: Programmatically override locale per module
import io.github.ismoy.imagepickerkmp.I18nKonfig
// Force a specific language (e.g. Dutch, Polish, Spanish)
I18nKonfig.setLocale("nl")
// 3. Access generated type-safe string resources
val title = I18nKonfig.General.camera_permission_required
val cancel = I18nKonfig.Common.cancel_option
Adding a New Language
Need a language not yet listed? Fork the repository and add your language code directly to the target module's translation file:
imagepickerkmp-photo/src/commonMain/resources/translations.yamlimagepickerkmp-video/src/commonMain/resources/translations.yamlimagepickerkmp-audio/src/commonMain/resources/translations.yamlimagepickerkmp-scanner/src/commonMain/resources/translations.yaml
camera_permission_required:
en: "Camera permission required"
es: "Permiso de cámara requerido"
pt: "Permissão de câmera necessária" # <-- Your new language
nl: "Cameratoestemming vereist"
rememberImagePickerKMP NEW
The idiomatic and recommended Compose entry point. Call rememberImagePickerKMP() in your composable and you get an ImagePickerKMPState — a stable state holder that controls when the picker opens, in which mode, and exposes the result reactively. No showCamera / showGallery booleans, no Render() call needed — the picker self-manages when you invoke launchCamera() or launchGallery().
rememberImagePickerKMP() is the only public API. No legacy wrappers needed.Full real-world example
A complete example of how to use rememberImagePickerKMP with camera and multi-gallery, handling all states and displaying results:
@Composable
fun MyScreen(innerPadding: PaddingValues) {
// 1. Create the picker with a global config
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
enableCrop = false,
galleryConfig = GalleryConfig(
allowMultiple = true,
selectionLimit = 10,
includeExif = true,
redactGpsData = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG)
)
)
)
// 2. Read the reactive result
val result = picker.result
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
// 3. React to each picker state
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
contentAlignment = Alignment.Center
) {
when (result) {
is ImagePickerResult.Loading -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Text("Selecting...", color = Color.Gray)
}
}
is ImagePickerResult.Success -> {
val photos = result.photos
if (photos.size == 1) {
// Single photo (camera or simple gallery)
val painter = photos.first().loadPainter()
if (painter != null) {
Image(painter = painter, contentDescription = "Captured photo")
}
} else {
// Multiple gallery photos
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(photos) { photo ->
val painter = photo.loadPainter()
if (painter != null) {
Image(painter = painter, contentDescription = null)
}
}
}
}
}
is ImagePickerResult.Error ->
Text("Error: ${result.exception.message}", color = Color.Red)
is ImagePickerResult.Dismissed,
is ImagePickerResult.Idle ->
Text("No image selected", color = Color.Gray)
}
}
// 4. Buttons that trigger the picker directly
Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Button(
onClick = { picker.launchCamera() },
modifier = Modifier.weight(1f)
) { Text("Camera") }
Button(
onClick = { picker.launchGallery() },
modifier = Modifier.weight(1f)
) { Text("Gallery") }
}
}
}
Per-launch overrides
You can override the global config for a specific launch without changing the base config:
// JPEG only, max 5 photos, with EXIF for this launch
picker.launchGallery(
allowMultiple = true,
selectionLimit = 5,
mimeTypes = listOf(MimeType.IMAGE_JPEG),
includeExif = true,
redactGpsData = false // GPS included just for this launch
)
// Camera with HIGH compression, only for this tap
// Crop is controlled via ImagePickerKMPConfig(cropConfig = CropConfig(enabled = true))
picker.launchCamera(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH,
includeExif = true
),
onDismiss = { /* specific callback on close */ },
onError = { e -> println("Error: ${e.message}") }
)
ImagePickerKMPConfig — Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
cameraCaptureConfig | CameraCaptureConfig | defaults | Camera behaviour: compression, EXIF, crop config |
galleryConfig | GalleryConfig | defaults | Multi-select, MIME types, selection limit, EXIF, redact GPS, compression |
cropConfig | CropConfig | defaults | Crop UI — set CropConfig(enabled = true) to activate crop after every capture or selection |
permissionAndConfirmationConfig | PermissionAndConfirmationConfig | defaults | Custom @Composable permission dialogs & handlers |
GalleryConfig — Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
allowMultiple | Boolean | false | Allow multiple file selection |
mimeTypes | List<MimeType> | [IMAGE_ALL] | Allowed file types |
selectionLimit | Int | 30 | Maximum selectable files (requires allowMultiple = true) |
includeExif | Boolean | false | Extract EXIF metadata from images |
redactGpsData | Boolean | true | Remove GPS (lat/lon/alt) from EXIF before delivering it |
compressionLevel | CompressionLevel? | null | Gallery image compression (LOW / MEDIUM / HIGH / null = no compression) |
mimeTypeMismatchMessage | String? | null | Custom message when the file does not match mimeTypes |
ImagePickerKMPState — Methods & properties
| Method / Property | Description |
|---|---|
result: ImagePickerResult | Reactive state. Starts as Idle. Observe with when. Updates automatically. |
isCropActive: Boolean | true while the crop UI is shown. Automatically resets to false when a final result is delivered or reset() is called. |
launchCamera(cameraCaptureConfig?, onDismiss?, onError?) | Opens the camera. All parameters are optional — they override the global config for this launch only. Crop is controlled globally via ImagePickerKMPConfig(cropConfig = CropConfig(enabled = true)). |
launchGallery(allowMultiple?, mimeTypes?, selectionLimit?, includeExif?, redactGpsData?, mimeTypeMismatchMessage?, cameraCaptureConfig?, onDismiss?, onError?) | Opens the gallery. Any parameter overrides the global GalleryConfig for this launch only. |
reset() | Resets result to Idle, clears isCropActive, and closes any active picker. |
Render() or launchPicker() in this API. The picker self-manages internally when you call launchCamera() or launchGallery(). No additional composable is needed.ImagePickerResult — State hierarchy
| State | When it occurs | Available data |
|---|---|---|
Idle | Initial state, before any action, and after reset() | — |
Loading | The picker is open and waiting for user selection | — |
Success | The user selected / captured images successfully | photos: List<PhotoResult>, first: PhotoResult? |
Dismissed | The user closed the picker without selecting anything | — |
Error | An error occurred during capture or selection | exception: Exception |
when (val result = picker.result) {
is ImagePickerResult.Idle ->
Text("No image selected", color = Color.Gray)
is ImagePickerResult.Loading ->
CircularProgressIndicator()
is ImagePickerResult.Success -> {
// result.photos: List<PhotoResult>
// result.first: PhotoResult? (first photo, useful for camera)
result.photos.forEach { photo ->
val painter = photo.loadPainter() // Painter for Compose
val bytes = photo.loadBytes() // ByteArray for file operations
val bitmap = photo.loadImageBitmap() // ImageBitmap for graphics
val base64 = photo.loadBase64() // Base64 for APIs
}
}
is ImagePickerResult.Dismissed ->
Text("Cancelled by user")
is ImagePickerResult.Error ->
Text("Error: ${result.exception.message}", color = Color.Red)
}
Before vs After
var showCamera by remember {
mutableStateOf(false)
}
var photo by remember {
mutableStateOf<PhotoResult?>(null)
}
if (showCamera) {
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = {
photo = it
showCamera = false
},
onError = { showCamera = false },
onDismiss = { showCamera = false }
)
)
}
Button(onClick = { showCamera = true }) {
Text("Camera")
}
val picker = rememberImagePickerKMP()
val imagePickerResponse = picker.result
Button(onClick = {
picker.launchCamera()
}) {
Text("Camera")
}
when(imagePickerResponse) {
is ImagePickerResult.Success ->
Image(imagePickerResponse.first!!.loadPainter()!!, null)
is ImagePickerResult.Loading ->
CircularProgressIndicator()
is ImagePickerResult.Error ->
Text("Error: ${imagePickerResponse.exception.message}", color = Color.Red)
is ImagePickerResult.Dismissed ->
Text("Cancelled") Text("Selection cancelled", color = Color.Gray)
is ImagePickerResult.Idle ->
Text("Press a button to get started", color = Color.Gray)
}
Camera Capture
Use rememberImagePickerKMP() to open the native camera. Control flash, skip confirmation, add crop and set compression.
Basic Camera
val picker = rememberImagePickerKMP()
Button(onClick = { picker.launchCamera() }) { Text("Camera") }
when (val result = picker.result) {
is ImagePickerResult.Success -> { val photo = result.photos.first() }
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
else -> {}
}
Camera with Compression & Crop
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.MEDIUM,
includeExif = true,
redactGpsData = false,
cropConfig = CropConfig(
enabled = true,
circularCrop = true,
squareCrop = true,
freeformCrop = false,
initialZoom = 1f,
constrainCropToImageBounds = true
)
)
)
)
Button(onClick = { picker.launchCamera() }) { Text("Camera") }
when (val result = picker.result) {
is ImagePickerResult.Success -> { val photo = result.photos.first() }
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
else -> {}
}
CameraCaptureConfig Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
compressionLevel | CompressionLevel? | null | Image compression level (LOW / MEDIUM / HIGH / null = no compression) |
includeExif | Boolean | false | Extract EXIF metadata upon capture |
redactGpsData | Boolean | true | Strip GPS coordinates from EXIF for privacy |
cropConfig | CropConfig | CropConfig() | Crop configuration for camera captures |
permissionAndConfirmationConfig | PermissionAndConfirmationConfig | PermissionAndConfirmationConfig() | Permission dialogs & handlers |
Gallery Picker
Use rememberImagePickerKMP() for single or multiple photo selection from the device's photo library.
Single Selection
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(allowMultiple = false)
)
)
Button(onClick = { picker.launchGallery() }) { Text("Gallery") }
when (val result = picker.result) {
is ImagePickerResult.Success -> { val photo = result.photos.first() }
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
else -> {}
}
Multiple Selection with MIME Filter
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
selectionLimit = 10,
mimeTypes = listOf(
MimeType.IMAGE_JPEG,
MimeType.IMAGE_PNG,
MimeType.IMAGE_WEBP
)
)
)
)
Button(onClick = { picker.launchGallery() }) { Text("Gallery") }
when (val result = picker.result) {
is ImagePickerResult.Success -> { result.photos.forEach { /* use photo */ } }
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
else -> {}
}
Image Crop
Built-in crop UI with free, square and circular modes plus zoom and rotation controls. Works on Android, iOS, Desktop and Web.
With rememberImagePickerKMP NEW
Enable crop globally in ImagePickerKMPConfig. Crop applies automatically after every capture or gallery selection.
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cropConfig = CropConfig(
enabled = true,
circularCrop = true,
squareCrop = true,
freeformCrop = true,
initialZoom = 1f,
minZoom = 1f,
constrainCropToImageBounds = true,
allowSkip = false
)
)
)
// The built-in editor blocks interactions and shows a Material 3
// wavy progress indicator while the confirmed crop is being processed.
Button(onClick = { picker.launchCamera() }) { Text("Camera + Crop") }
Button(onClick = { picker.launchGallery() }) { Text("Gallery + Crop") }
With rememberImagePickerKMP (crop enabled)
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cropConfig = CropConfig(enabled = true)
)
)
picker.launchCamera()
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the crop editor after capture or selection |
aspectRatioLocked | Boolean | false | Lock the selected crop aspect ratio while resizing |
circularCrop | Boolean | true | Show circular crop mode (uses a 1:1 selection) |
squareCrop | Boolean | true | Show square crop mode |
freeformCrop | Boolean | false | Show free-form crop mode and aspect-ratio presets |
initialZoom | Float | 1f | Initial image zoom in the crop editor; clamped to the supported range |
minZoom | Float | 1f | Lowest selectable image zoom in the crop editor |
constrainCropToImageBounds | Boolean | true | Keep the crop region within the rendered image bounds |
allowSkip | Boolean | false | Show Skip to return the pending image without cropping it |
rememberImagePickerKMP: when crop is active,
picker.isCropActive is true and picker.result is reset to Idle until the user confirms or cancels the crop.
Once the crop operation completes, isCropActive returns to false and result transitions to Success, Dismissed, or Error.
Use this flag to coordinate UI outside the editor while crop is active. The editor itself blocks input and shows a Material 3 wavy progress indicator after the user confirms the crop, until processing completes.
Image Compression
Optional background compression with configurable levels. Works for camera and gallery on Android and iOS. Compression is disabled by default — the original file is returned untouched.
// Camera with compression
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH
)
)
)
picker.launchCamera()
// Gallery with compression — use GalleryConfig, not CameraCaptureConfig
val galleryPicker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
compressionLevel = CompressionLevel.MEDIUM
)
)
)
galleryPicker.launchGallery()
| Level | JPEG Quality | Max Dimension | Use Case |
|---|---|---|---|
null (default) | — | original | No compression — original file returned as-is |
LOW | 85% | 3840 px (4K) | Near-lossless, large files |
MEDIUM | 70% | 1920 px (FHD) | Balanced quality/size — recommended |
HIGH | 50% | 1280 px (HD) | Maximum size reduction |
CameraCaptureConfig and GalleryConfig default to compressionLevel = null — the original image is returned without any resize or re-encoding. Set an explicit level to enable compression. Camera and gallery compression are configured independently.EXIF Metadata
Extract rich metadata from photos on Android and iOS. Requires includeExif = true.
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(includeExif = true)
)
)
picker.launchCamera()
when (val result = picker.result) {
is ImagePickerResult.Success -> {
result.photos.first().exif?.let { exif ->
println("GPS: ${exif.latitude}, ${exif.longitude}")
println("Camera: ${exif.cameraModel}")
println("Date: ${exif.dateTaken}")
println("Flash: ${exif.flash}")
println("ISO: ${exif.iso}")
println("Shutter: ${exif.shutterSpeed}")
println("Size: ${exif.imageWidth} x ${exif.imageHeight} px")
}
}
else -> {}
}
// Gallery EXIF
val galleryPicker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(includeExif = true)
)
)
galleryPicker.launchGallery()
| Field | Type | Description |
|---|---|---|
| GPS | ||
latitude | Double? | GPS latitude — redacted by default (redactGpsData = true) |
longitude | Double? | GPS longitude — redacted by default |
altitude | Double? | GPS altitude in meters — redacted by default |
| Date & Time | ||
dateTaken | String? | Date and time photo was taken |
dateTime | String? | General date/time (alias of dateTaken) |
digitizedTime | String? | Date image was digitized |
modifiedTime | String? | Last modified date |
| Camera | ||
cameraModel | String? | Camera/device model |
cameraManufacturer | String? | Camera manufacturer |
software | String? | Processing software |
| Capture Settings | ||
flash | String? | Flash status |
iso | String? | ISO sensitivity |
aperture | String? | Aperture f-stop value |
shutterSpeed | String? | Shutter speed (exposure time) |
focalLength | String? | Focal length in mm |
whiteBalance | String? | White balance setting |
exposureBias | String? | Exposure compensation |
meteringMode | String? | Metering mode used |
| Image Properties | ||
imageWidth | Int? | Original width in pixels |
imageHeight | Int? | Original height in pixels |
orientation | String? | Image rotation/orientation |
colorSpace | String? | Color space (sRGB, Adobe RGB, etc.) |
thumbnail | String? | Base64 thumbnail (~5–20 KB). Avoid caching. |
latitude, longitude and altitude are set to null unless you explicitly set redactGpsData = false in CameraCaptureConfig or GalleryConfig. Only disable redaction when the user has been clearly informed.Extension Functions
Process PhotoResult and GalleryPhotoResult with built-in extensions for UI rendering, file system paths, and kotlinx-io streaming.
val picker = rememberImagePickerKMP()
when (val result = picker.result) {
is ImagePickerResult.Success -> {
val photo = result.photos.first()
// ── UI & Compose ──────────────────────────────────────────────
val painter: Painter? = photo.loadPainter()
val bitmap: ImageBitmap? = photo.loadImageBitmap()
// ── Data Conversion ──────────────────────────────────────────
val bytes: ByteArray = photo.loadBytes()
val base64: String = photo.loadBase64()
// ── File System & Paths ───────────────────────────────────────
val absPath: String = photo.absolutePath // Direct file path string
val path: Path = photo.asPath() // kotlinx.io.files.Path
val fileExists: Boolean = photo.exists() // Check if file exists on disk
// ── kotlinx-io Streaming ──────────────────────────────────────
val source: Source = photo.asSource() // Buffered Source
val rawSource: RawSource = photo.asRawSource() // Unbuffered RawSource
// Stream directly to a RawSink (file upload / copy)
val destinationSink = SystemFileSystem.sink(Path("destination.jpg"))
photo.transferToSink(destinationSink)
}
else -> {}
}
| Extension | Return Type | Description |
|---|---|---|
photo.loadPainter() | Painter? | Decodes image into Compose Painter for Image() composables |
photo.loadImageBitmap() | ImageBitmap? | Decodes image into Compose ImageBitmap for Canvas rendering |
photo.loadBytes() | ByteArray | Reads complete file content into a byte array |
photo.loadBase64() | String | Encodes image data to Base64 string for REST API payloads |
photo.absolutePath | String | Returns direct platform file path (resolves content:// and file://) |
photo.asPath() | Path | Converts absolutePath into a kotlinx.io.files.Path |
photo.exists() | Boolean | Verifies if the file exists on the local file system |
photo.asSource() | Source | Opens a buffered kotlinx.io.Source for high-performance reading |
photo.asRawSource() | RawSource | Opens an unbuffered kotlinx.io.RawSource for low-level file reading |
photo.transferToSink(sink) | Unit | Streams the photo content directly into any kotlinx.io.RawSink |
MIME Types
Use MimeType enum values to filter what files can be selected in rememberImagePickerKMP via GalleryConfig.
| Value | MIME String | Description |
|---|---|---|
MimeType.IMAGE_ALL | image/* | All image formats (default) |
MimeType.IMAGE_JPEG | image/jpeg | JPEG images |
MimeType.IMAGE_PNG | image/png | PNG images |
MimeType.IMAGE_WEBP | image/webp | WebP images |
MimeType.IMAGE_GIF | image/gif | GIF images |
MimeType.IMAGE_BMP | image/bmp | BMP images |
MimeType.IMAGE_HEIC | image/heic | HEIC — iOS native format |
MimeType.IMAGE_HEIF | image/heif | HEIF — iOS native format |
MimeType.APPLICATION_PDF | application/pdf | PDF documents |
Utility Methods
// Convert to string list
val strings = MimeType.toMimeTypeStrings(
MimeType.IMAGE_JPEG,
MimeType.IMAGE_PNG
)
// → ["image/jpeg", "image/png"]
// Parse from string
val mt = MimeType.fromString("image/webp")
// → MimeType.IMAGE_WEBP
// Predefined groups
MimeType.COMMON_IMAGE_TYPES // JPEG, PNG, GIF, WebP
MimeType.ALL_SUPPORTED_TYPES // all enum entries
UI Customization
Customize permission handling, denied dialogs, settings prompts, and localized dialog strings.
PermissionAndConfirmationConfig — Permission & Dialog Options
Configure custom permission handlers, denied dialogs, settings redirection, and iOS alert labels. All properties are optional.
| Property | Type | Default | Description |
|---|---|---|---|
customPermissionHandler | ((PermissionConfig) -> Unit)? | null | Custom permission check/request callback receiving localized PermissionConfig |
customDeniedDialog | @Composable ((onRetry: ()->Unit, onDismiss: ()->Unit) -> Unit)? | null | Custom Composable dialog shown when permission is denied |
customSettingsDialog | @Composable ((onOpenSettings: ()->Unit, onDismiss: ()->Unit) -> Unit)? | null | Custom Composable dialog shown when permission is permanently denied |
cancelButtonTextIOS | String? | null | iOS permission alert cancel button label override |
onCancelPermissionConfigIOS | (() -> Unit)? | null | iOS callback executed when user cancels the permission alert |
1 — Custom Permission Denied Dialog
Receives onRetry to re-trigger the permission request and onDismiss to cancel.
PermissionAndConfirmationConfig(
customDeniedDialog = { onRetry, onDismiss ->
Dialog(onDismissRequest = onDismiss) {
Card(shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Camera Permission Required", fontWeight = FontWeight.Bold)
Spacer(Modifier.height(12.dp))
Text("Please grant camera access to take photos.", color = Color.Gray)
Spacer(Modifier.height(20.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = onDismiss, modifier = Modifier.weight(1f)) { Text("Cancel") }
Button(onClick = onRetry, modifier = Modifier.weight(1f)) { Text("Retry") }
}
}
}
}
}
)
2 — Custom Settings Dialog (Permanently Denied)
Receives onOpenSettings to navigate to platform app settings and onDismiss to cancel.
PermissionAndConfirmationConfig(
customSettingsDialog = { onOpenSettings, onDismiss ->
Dialog(onDismissRequest = onDismiss) {
Card(shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Permission Permanently Denied", fontWeight = FontWeight.Bold)
Spacer(Modifier.height(12.dp))
Text("Open system settings to enable camera access manually.", color = Color.Gray)
Spacer(Modifier.height(20.dp))
Button(onClick = onOpenSettings, modifier = Modifier.fillMaxWidth()) {
Text("Open Settings")
}
}
}
}
}
)
PermissionConfig — Localized Strings
Holds localized titles, descriptions, and button strings for permission dialogs.
// Default PermissionConfig automatically resolves localized strings for device language
val config = PermissionConfig()
// Custom PermissionConfig (override specific strings)
val customConfig = PermissionConfig(
titleDialogConfig = "Camera Permission",
descriptionDialogConfig = "We need camera access to capture photos.",
btnDialogConfig = "Open Settings",
titleDialogDenied = "Permission Denied",
descriptionDialogDenied = "Camera permission is required.",
btnDialogDenied = "Grant Permission",
btnCancel = "Cancel"
)
result.uri (not .image), result.fileSize in bytes (not KB), result.mimeType (not .format), result.width, result.height.Gallery Options — GalleryConfig
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
selectionLimit = 10, // iOS only — max 10 items
includeExif = true,
redactGpsData = false, // expose GPS (inform user!)
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG)
)
)
)
Button(onClick = { picker.launchGallery() }) { Text("Gallery") }
| Parameter | Type | Default | Description |
|---|---|---|---|
allowMultiple | Boolean | false | Enable multi-file selection |
mimeTypes | List<MimeType> | [IMAGE_ALL] | Allowed file types |
selectionLimit | Int | 30 | Max items when allowMultiple (iOS) |
includeExif | Boolean | false | Extract EXIF metadata |
redactGpsData | Boolean | true | Strip GPS from EXIF (privacy default) |
React / Web Integration
ImagePickerKMP is available as an NPM package for JavaScript/TypeScript projects including React, Vue, Angular and Vanilla JS.
Installation
npm install imagepickerkmp
React Component
import { useImagePicker } from 'imagepickerkmp';
function PhotoUploader() {
const { openCamera, openGallery, result } = useImagePicker({
onPhotoCaptured: (photo) => {
console.log('Photo:', photo.fileName, photo.fileSize);
},
onError: (err) => console.error(err)
});
return (
<div>
<button onClick={openCamera}>Open Camera</button>
<button onClick={openGallery}>Open Gallery</button>
{result && <img src={result.uri} alt="captured" />}
</div>
);
}
WebRTC Camera
Browser camera via WebRTC. Works on mobile & desktop.
Drag & Drop
File picker with drag and drop support.
TypeScript
Full type definitions included out of the box.
Cross-Framework
React, Vue, Angular, Vanilla JS.
Video — imagepickerkmp-video Coming Soon
Video recording via the native camera and gallery video picking. Returns a VideoResult with duration, dimensions, file size and codec metadata.
implementation("io.github.ismoy:imagepickerkmp-video:TBD" // coming soon — not yet published)
iOS — Info.plist
<key>NSCameraUsageDescription</key>
<string>Required to record video.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Required to record audio in videos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Required to select videos from your library.</string>
Basic Usage
val picker = rememberVideoPicker(
config = VideoPickerConfig(
quality = VideoQuality.FULL_HD_1080P,
allowedMimeTypes = listOf(VideoMimeType.MP4),
audio = AudioConfig.Default,
output = VideoOutputConfig(format = VideoOutputFormat.MP4),
validation = VideoValidationConfig(
maxDuration = 60.seconds,
maxFileSize = 100.mb
)
)
)
Button(onClick = { picker.launchCamera() }) { Text("Record") }
Button(onClick = { picker.launchGallery() }) { Text("Pick Video") }
when (val state = picker.result) {
is VideoPickerState.Success -> Text("${state.video.fileName} — ${state.video.durationMs / 1000}s")
is VideoPickerState.Error -> Text("Error: ${state.cause}")
is VideoPickerState.Cancelled -> Text("Cancelled")
else -> {}
}
| State | When |
|---|---|
Idle | Initial or after reset() |
Loading | Picker is open |
Success(video) | Video selected / recorded — contains VideoResult |
Cancelled | User dismissed |
Error(cause) | Error or validation failure |
Audio — imagepickerkmp-audio Coming Soon
Two independent APIs for audio recording with waveform visualization and file picking. Includes a built-in AudioPlayer composable.
implementation("io.github.ismoy:imagepickerkmp-audio:TBD" // coming soon — not yet published)
1. Inline widget — AudioRecorder
Embeds a hold-to-record mic button directly in your layout. No dialog, no state holder.
AudioRecorder(
config = AudioRecorderConfig(
gesture = RecordGesture.HoldAndSlide,
showWaveform = true,
showDuration = true
),
onResult = { audioResult: AudioResult? ->
if (audioResult != null) {
println("Saved: ${audioResult.uri}, ${audioResult.durationMs}ms")
}
}
)
2. Modal state-holder — rememberAudioPicker
val picker = rememberAudioPicker(
config = AudioPickerConfig(
validation = AudioPickerValidation(
maxFileSizeBytes = 20 * 1024 * 1024L,
maxDurationMs = 300_000L
)
),
recorderConfig = AudioRecorderConfig(showWaveform = true)
)
Button(onClick = { picker.launchRecorder() }) { Text("Record") }
Button(onClick = { picker.launchGallery() }) { Text("Pick File") }
when (val state = picker.result) {
is AudioPickerState.Success -> {
val audio = state.audio
Text("${audio.fileName} — ${audio.durationMs / 1000}s")
ImagePickerAudioPlayer(
state = rememberAudioPlayerState(
AudioSource.Local(
uri = audio.uri,
durationMs = audio.durationMs,
title = audio.fileName
)
),
layout = AudioPlayerLayout.VoiceMessage()
) }
is AudioPickerState.Error -> Text("Error: ${state.cause}")
is AudioPickerState.Cancelled -> Text("Cancelled")
else -> {}
}
| Player Layout | Best for |
|---|---|
AudioPlayerLayout.VoiceMessage | Chat apps, read/unread status |
AudioPlayerLayout.Compact | Toolbars, minimal space |
AudioPlayerLayout.Music | Full player with cover art |
AudioPlayerLayout.Podcast | Podcast / audiobook with skip controls |
Scanner — imagepickerkmp-scanner Available
Live barcode and QR code scanning via the camera for Android (minSdk 24) and iOS. Supports 18 barcode formats, batch mode, and static scanning from an image ByteArray.
implementation("io.github.ismoy:imagepickerkmp-scanner:1.0.0")
Basic Usage
val scanner = rememberScannerPicker(
config = ScannerPickerConfig(
camera = ScannerCameraConfig(
behavior = ScannerBehaviorConfig(
allowedFormats = listOf(BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13),
playSound = true
)
)
)
)
Button(onClick = { scanner.launchScanner() }) { Text("Scan") }
when (val result = scanner.result) {
is ScannerPickerState.Success -> Text("${result.result.code} (${result.result.format})")
is ScannerPickerState.BatchSuccess -> result.results.forEach { Text(it.code) }
is ScannerPickerState.Cancelled -> Text("Cancelled")
is ScannerPickerState.Error -> Text("Error: ${result.error}")
else -> {}
}
createStaticCodeScanner().scanImage(bytes) to decode a barcode from a ByteArray without opening the camera. Available on Android and iOS.NSCameraUsageDescription to Info.plist with a user-facing reason, such as “Required to scan barcodes and QR codes.”Audio Player — imagepickerkmp-audio-player Coming Soon
Low-level audio playback engine. Use this module when you need full control over the player UI — play/pause/seek/speed from your own composables. The built-in ImagePickerAudioPlayer in imagepickerkmp-audio is powered by this same engine.
| Use case | Module |
|---|---|
| Ready-made voice message / podcast / music UI | imagepickerkmp-audio — ImagePickerAudioPlayer |
| Custom player UI built from scratch | imagepickerkmp-audio-player — rememberAudioPlayerState |
implementation("io.github.ismoy:imagepickerkmp-audio-player:TBD" // coming soon — not yet published)
Usage — rememberAudioPlayerState
@Composable
fun CustomPlayerScreen(audioUri: String, durationMs: Long) {
val playerState = rememberAudioPlayerState(
AudioSource.Local(
uri = audioUri,
durationMs = durationMs,
title = "Voice Memo"
)
)
val isPlaying = playerState.isPlaying
val position = playerState.currentPositionMs
val duration = playerState.durationMs
val progress = if (duration > 0) position.toFloat() / duration else 0f
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
Button(onClick = {
if (isPlaying) playerState.pause() else playerState.play()
}) { Text(if (isPlaying) "Pause" else "Play") }
// Seek from a button
TextButton(onClick = { playerState.seekTo(0) }) { Text("Restart") }
}
AudioSource types
| Type | When to use |
|---|---|
AudioSource.Local(uri, durationMs, title, waveform?) | Local file — from recording or gallery. Pass waveform to show amplitude bars. |
AudioSource.Url(url, title) | Remote streaming URL (HTTP/HTTPS) |
AudioSource.Playlist(items) | Ordered list of AudioSource for multi-track playback |
PlayerState — properties & methods
| Method / Property | Description |
|---|---|
isPlaying: Boolean | Whether audio is currently playing |
currentPositionMs: Long | Current playback position in milliseconds |
durationMs: Long | Total duration in milliseconds |
play() | Start or resume playback |
pause() | Pause at current position |
seekTo(positionMs) | Seek to an arbitrary position |
rememberAudioPlayerState with the built-in layouts. Pass it directly to ImagePickerAudioPlayer(state = ..., layout = ...) if you want separate state control while still using a ready-made player UI.Video Player — imagepickerkmp-video-player Coming Soon
Full-featured video playback. Supports HTTP/HLS/DASH streaming, local files, playlists, PiP, quality selection, fullscreen, pinch-to-zoom, and custom UI slots.
implementation("io.github.ismoy:imagepickerkmp-video-player:TBD" // coming soon — not yet published)
Basic Usage
// Simple — just pass a source
ImagePickerVideoPlayer(
source = VideoSource.Url("https://example.com/video.mp4"),
config = VideoPlayerConfig(
behavior = VideoBehaviorConfig(autoPlay = true, enablePinchToZoom = true)
)
)
// With programmatic control
val player = rememberVideoPlayerState(source = VideoSource.Url(videoUrl))
ImagePickerVideoPlayer(state = player, config = VideoPlayerConfig())
Button(onClick = { player.play() }) { Text("Play") }
Button(onClick = { player.pause() }) { Text("Pause") }
Button(onClick = { player.seekTo(30_000L) }) { Text("Seek 30s") }
Button(onClick = { player.toggleFullscreen() }) { Text("Fullscreen") }
Button(onClick = { player.enterPiP() }) { Text("PiP") }
| Source Type | Example |
|---|---|
VideoSource.Url | HTTP/HTTPS, HLS .m3u8, DASH .mpd |
VideoSource.Local | File path or Android content:// URI |
VideoSource.Playlist | Ordered list of VideoSource items |
Custom Layout (TikTok / Reels HUD style)
Use VideoUIExtensions(customLayout = { uiState -> ... }) to replace the entire player UI. Access playback state and controls through uiState.
val config = VideoPlayerConfig(
behavior = VideoBehaviorConfig(
autoPlay = true,
enablePinchToZoom = true,
repeatToggleModes = RepeatToggleModes.ONE_AND_ALL
),
uiExtensions = VideoUIExtensions(
customLayout = { uiState ->
val isPlaying = uiState.status == VideoPlayerStatus.Playing
Box(modifier = Modifier.fillMaxSize()) {
// Custom play/pause button on the right edge
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
.clickable { if (isPlaying) uiState.pause() else uiState.play() }
.align(Alignment.CenterEnd),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
tint = Color.White
)
}
// Position counter
Text(
text = "${uiState.currentPosition / 1000}s",
color = Color.White,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
)
)
ImagePickerVideoPlayer(source = VideoSource.Url("https://example.com/video.mp4"), config = config)
Platform Support Matrix
| Feature | Android | iOS | Desktop | JS/Web | WASM |
|---|---|---|---|---|---|
| Camera Capture | |||||
| Gallery Picker | |||||
| Crop UI | |||||
| EXIF Metadata | |||||
| Compression | |||||
| Multiple Selection | |||||
Video — imagepickerkmp-video — coming soon | |||||
| Video — Camera Record | |||||
| Video — Gallery Pick | |||||
Audio — imagepickerkmp-audio — coming soon | |||||
| Audio — Record | |||||
| Audio — Gallery Pick | |||||
Audio Player — imagepickerkmp-audio-player — coming soon | |||||
| Audio Playback | |||||
Scanner — imagepickerkmp-scanner | |||||
| QR / Barcode Scan | |||||
Video Player — imagepickerkmp-video-player — coming soon | |||||
| HTTP/HTTPS Streaming | |||||
| Local File Playback | |||||
| Picture-in-Picture | |||||
API Reference
rememberImagePickerKMP NEW
Recommended entry point. See the rememberImagePickerKMP section for full examples and configuration details.
| Method / Property | Description |
|---|---|
result: ImagePickerResult | Reactive picker state: Idle | Loading | Success | Dismissed | Error |
isCropActive: Boolean | true while the crop UI is active. Resets to false on final result or reset(). |
launchCamera(cameraCaptureConfig?, onDismiss?, onError?) | Opens the camera. All parameters are optional and override the global config for this launch only. Crop is set globally via cropConfig = CropConfig(enabled = true). |
launchGallery(allowMultiple?, mimeTypes?, selectionLimit?, includeExif?, redactGpsData?, mimeTypeMismatchMessage?, cameraCaptureConfig?, onDismiss?, onError?) | Opens the gallery. All parameters are optional and override the global GalleryConfig for this launch only. |
reset() | Resets result to Idle, clears isCropActive, and closes any active picker. |
ImagePickerKMPConfig — Main Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
cameraCaptureConfig | CameraCaptureConfig | CameraCaptureConfig() | Camera capture, compression & EXIF settings |
galleryConfig | GalleryConfig | GalleryConfig() | Gallery selection, MIME filtering & limit settings |
cropConfig | CropConfig | CropConfig() | Post-capture & post-selection crop UI settings |
permissionAndConfirmationConfig | PermissionAndConfirmationConfig | PermissionAndConfirmationConfig() | Permission handler & custom dialog configurations |
CameraCaptureConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
compressionLevel | CompressionLevel? | CompressionLevel.LOW | Image compression level (LOW / MEDIUM / HIGH / null) |
includeExif | Boolean | false | Extract EXIF metadata upon capture |
redactGpsData | Boolean | true | Strip GPS coordinates from EXIF for privacy |
permissionAndConfirmationConfig | PermissionAndConfirmationConfig | …() | Custom permission dialogs & handlers |
cropConfig | CropConfig | CropConfig() | Crop configuration after camera capture |
GalleryConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
allowMultiple | Boolean | false | Enable multi-file selection |
mimeTypes | List<MimeType> | [IMAGE_ALL] | Allowed MIME types |
selectionLimit | Int | 30 | Maximum selection limit (iOS) |
includeExif | Boolean | false | Extract EXIF metadata upon gallery pick |
redactGpsData | Boolean | true | Strip GPS coordinates from EXIF |
mimeTypeMismatchMessage | String? | null | Custom warning message when unselected MIME type is picked |
PermissionAndConfirmationConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
customPermissionHandler | ((PermissionConfig) -> Unit)? | null | Custom callback to manage permission request flow |
customDeniedDialog | @Composable ((onRetry: ()->Unit, onDismiss: ()->Unit) -> Unit)? | null | Custom dialog when permission is denied |
customSettingsDialog | @Composable ((onOpenSettings: ()->Unit, onDismiss: ()->Unit) -> Unit)? | null | Custom dialog when permission is permanently denied |
cancelButtonTextIOS | String? | null | Custom cancel text for iOS alert dialogs |
onCancelPermissionConfigIOS | (() -> Unit)? | null | iOS callback when permission alert is cancelled |
CropConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable crop UI after capture or gallery pick |
aspectRatioLocked | Boolean | false | Lock aspect ratio during crop manipulation |
circularCrop | Boolean | true | Enable circular crop handle/overlay |
squareCrop | Boolean | true | Enable square 1:1 crop preset |
freeformCrop | Boolean | false | Enable freeform ratio cropping |
PhotoResult / GalleryPhotoResult
GalleryPhotoResult is a typealias for PhotoResult — both camera and gallery results use the same object model.
| Field / Property | Type | Description |
|---|---|---|
uri | String | Platform-native URI string of the selected/captured file |
fileName | String? | File name with extension (e.g., "photo.jpg") |
fileSize | Long? | File size in bytes (divide by 1024 for KB) |
mimeType | String? | MIME type string (e.g. "image/jpeg", "image/png") |
width | Int? | Image width in pixels |
height | Int? | Image height in pixels |
exif | ExifData? | EXIF metadata — populated when includeExif = true |
absolutePath | String (Ext) | Absolute file system path as String |
asPath() | Path (Ext) | Converts to kotlinx.io.files.Path |
exists() | Boolean (Ext) | Checks if file exists on disk |
loadBytes() | ByteArray (Ext) | Reads complete file content into ByteArray |
loadBase64() | String (Ext) | Encodes file content to Base64 String |
loadPainter() | Painter? (Ext) | Decodes into Compose Painter |
loadImageBitmap() | ImageBitmap? (Ext) | Decodes into Compose ImageBitmap |
asSource() | Source (Ext) | Opens a buffered kotlinx.io.Source |
transferToSink(sink) | Unit (Ext) | Streams file data directly to a kotlinx.io.RawSink |
imagepicker-core Architecture
The core infrastructure module (io.github.ismoy:imagepicker-core) provides cross-platform abstractions used by all modules:
| Component | Package | Description |
|---|---|---|
CoreServices | core | Service locator providing access to PermissionManager, FileSystemManager, and MediaLogger |
PermissionManager | core.permissions | Abstracts platform permission checks & requests (Camera, Photos) |
FileSystemManager | core.filesystem | Abstracts file creation, URI resolution, temporary storage, and path mapping |
MediaLogger | core.logger | Internal structured console logger with configurable LogLevel |
PlatformFile | core.filesystem | Cross-platform file abstraction wrapper |
PlatformUri | core.uri | Cross-platform URI resolution helper |
fileSize returns bytes (previously KB). Migrate: val sizeKB = (result.fileSize ?: 0) / 1024.0ExifData Fields
All fields are nullable. Availability depends on the device, image origin and whether redactGpsData is false.
| Field | Type | Description |
|---|---|---|
| GPS & Location | ||
latitude | Double? | GPS latitude (stripped if redactGpsData = true) |
longitude | Double? | GPS longitude |
altitude | Double? | GPS altitude in metres |
| Date & Time | ||
dateTaken | String? | Original date/time when the photo was taken (yyyy:MM:dd HH:mm:ss) |
dateTime | String? | General date/time — alias of dateTaken for compatibility |
digitizedTime | String? | Date image was digitized |
originalTime | String? | Original creation time |
modifiedTime | String? | Last modified date |
utcTime | String? | UTC date and time |
| Camera | ||
cameraManufacturer | String? | Camera manufacturer (also exposed as cameraMake) |
cameraMake | String? | Alias for cameraManufacturer |
cameraModel | String? | Camera / device model name |
software | String? | Software used to produce the image |
owner | String? | Owner / copyright information |
focalLength | String? | Focal length in mm |
aperture | String? | Aperture (f-number) |
| Capture Settings | ||
iso | String? | ISO sensitivity |
shutterSpeed | String? | Shutter speed |
exposureBias | String? | Exposure compensation (EV) |
meteringMode | String? | Metering mode (multi, spot, etc.) |
flash | String? | Flash fired / not fired |
whiteBalance | String? | White balance mode |
sceneCaptureType | String? | Scene capture type (standard, landscape, portrait, night) |
| Image Properties | ||
imageWidth | Int? | Image pixel width |
imageHeight | Int? | Image pixel height |
orientation | String? | EXIF orientation (1–8) |
colorSpace | String? | Color space (sRGB, Adobe RGB, etc.) |
xResolution | String? | Horizontal resolution (DPI) |
yResolution | String? | Vertical resolution (DPI) |
resolutionUnit | String? | Resolution unit (inches / cm) |
compression | String? | Compression method |
thumbnail | String? | Base64 encoded thumbnail data (~5–20 KB). Avoid caching. |
Changelog
Recent releases and what changed in each version. Full history on GitHub Releases.
- New:
imagepickerkmp-video — coming soon— video recording and gallery video picking.rememberVideoPicker(),VideoPickerConfig, quality presets (360p–1080p),VideoResultwith codec info, validation constraints. - New:
imagepickerkmp-audio — coming soon— audio recording with real-time waveform. Two APIs: inlineAudioRecorderwidget (hold-to-record / tap-to-record) and modalrememberAudioPicker(). Includes built-inAudioPlayerwith four layouts (VoiceMessage, Compact, Music, Podcast). - New:
imagepickerkmp-audio-player — coming soon— low-level audio playback engine.rememberAudioPlayerManager()with fullPlaybackState(position, duration, speed) for custom player UIs. - New:
imagepickerkmp-video-player — coming soon— full-featured video playback. HLS/DASH streaming, playlist, PiP (Android 8+ / iOS 14+), quality selector, fullscreen, pinch-to-zoom, custom UI slots.
- New:
imagepickerkmp-scanner— live barcode and QR code scanning withrememberScannerPicker(), 18 barcode formats, batch mode, and static image scanning. Available on Android (minSdk 24) and iOS.
- Feature: Expanded
CropConfigwith initial and minimum zoom, image-bound crop constraints, optional Skip, and aspect-ratio locking controls. - Feature: Crop selections now start at a usable fitted size, remain constrained to the displayed image when configured, and support refined zoom and rotation controls.
- UX: Confirming a crop now blocks interaction with a Material 3 wavy progress indicator while the image is processed.
- Performance (Android): Rotated crops render directly to the output bitmap, avoiding a full-size intermediate rotation bitmap.
- Fix (iOS) #151, #158: Accepting camera permission no longer reports
ImagePickerResult.Dismissedbefore the camera capture completes; actual user cancellation still reportsDismissed.
- New: Modular Internationalization (i18n) via
i18nKonfig— each module (photo,video,audio,scanner) now bundles its own localizedtranslations.yamlwith zero bloat and automatic device language detection viagetLanguageDevice(). - New: Dutch (
nl) language translation support across all module strings and dialogs (contributed by@x-sheepin #156). - New: Type-safe programmatic locale switching with
I18nKonfig.setLocale(...)per module. - Fix: Common test and multiplatform compilation fixes for all targets (Android, iOS, JVM, WASM, JS).
- New: Polish (
pl) language translation support for camera, gallery, permissions, and error dialogs (contributed by@b1jaroszin #154). - Chore: Updated community contributors list and attribution.
- Fix (Android) #152: Resolved gallery picker crash by supporting wrapped
LocalContextinstances (e.g.ContextWrapper, custom themed activity contexts) inPlatformGalleryRenderer.android.kt(contributed by@Magmi183). - Docs: Comprehensive README updates with platform notes, permissions setup, and memory optimization guidelines.
- Refactor: Cleanly separated camera compression and gallery compression configuration in
ImagePickerKMPConfig(CameraCaptureConfig.compressionLevelvsGalleryConfig.compressionLevel). - Fix (Android) #149: Guarded camera launch against crashes when no camera app is installed on the host device via
reportLaunchFailurehelper (contributed by@jadlr). - Docs: Enhanced README with visual elements, badges, and detailed extension function guides.
- Performance (Memory): Optimized photo crop memory footprint to prevent out-of-memory issues on high-resolution camera captures.
- CI / CD: Fixed GitHub Actions auto-release workflow tagging and automated release publishing.
- Architecture: Stabilized modular inter-module dependencies between
imagepicker-coreandimagepickerkmp-photo. - Fix (JS/Web) #144: Fixed
ClassCastExceptionon JavaScript / Web target during photo file size calculation (contributed by@cloudigits).
- Breaking: Project refactored into a modular ecosystem. The library is now split into
imagepicker-core(shared infrastructure) andimagepickerkmp-photo(photo capture, gallery, crop, EXIF, compression). The dependency artifactio.github.ismoy:imagepickerkmpcontinues to work seamlessly. - New:
imagepicker-coremodule — shared permissions, filesystem, URI handling, and logging across modules. - New: Auto-initialization on Android via
ContentProvider(CoreInitializer) — manual initialization no longer required. - New: Modular Internationalization (i18n) via
i18nKonfig— each module packages its own localized string keys with zero bloat and auto-detects device locale (14 languages: EN, ES, FR, ZH, KO, JA, HI, TH, IT, DE, RU, UK, PL, NL). - New: Tag-based auto-release CI workflow for streamlined automated publishing.
- Fix (iOS) #135: Gallery picker is dramatically faster and no longer spikes memory. Image processing now uses ImageIO hardware-accelerated downsampling (decodes directly at target size from the file) instead of decoding the full-resolution bitmap into memory.
- Fix (iOS): Multi-image selection now processes with bounded parallelism, keeping peak memory low even when selecting many high-resolution photos.
- Fix (iOS): Picker launch uses a lightweight
PHPickerConfiguration, removing the cold-start delay on first open. EXIF is read directly from the file viaCGImageSource. - Fix (iOS): Resolved a false-positive dismissal during the presentation animation that could freeze or cancel the picker.
- Fix (Android) #136:
PhotoResult.absolutePathno longer crashes withFileNotFoundExceptionfor cropped images. Raw absolute file paths (e.g. cropped files in cacheDir) are now handled alongsidecontent://andfile://URIs.
- Breaking: Legacy API removed from public surface.
ImagePickerLauncherandGalleryPickerLauncherare nowinternal— onlyrememberImagePickerKMP()is the public API. - Breaking:
ImagePickerConfigis nowinternal. UseImagePickerKMPConfiginstead. - New: Internal functions renamed to
PlatformCameraRenderer/PlatformGalleryRendererfor clarity in the open-source codebase. - New: WasmJs target fully implemented — camera and gallery now work in browser via HTML file input.
- New: WasmJs compilation enabled (was previously disabled).
- Fix (iOS): Crop crash after camera capture resolved — dismiss camera completion handler now fires before opening crop Dialog, preventing "Unbalanced calls to begin/end appearance transitions".
- New:
CameraScaleTypeenum — controls camera preview scaling (FILL_CENTER,FIT_CENTER, etc.). Android only. - New:
PermissionAndConfirmationConfig.confirmationImageContentScale— controls how captured photo is scaled in confirmation screen. - Docs: All documentation updated to only show the new
rememberImagePickerKMPAPI. Legacy examples removed.
- New:
PhotoResult.absolutePathextension — returns the absolute file system path as a String for direct file access without URI parsing. - Platform implementations: Android uses ContentResolver to resolve content:// URIs, iOS uses URL.path, Desktop/Web use direct path extraction from file:// URIs.
- Complements: Works alongside the existing
toPath()extension (v1.0.38) for kotlinx-io compatibility.
- Fix (Android 7–11): Camera preview was blank/black on Android 11 (API 30) and below. Root cause:
PreviewViewwas hardcoded toImplementationMode.PERFORMANCE(SurfaceView), which does not render inside Jetpack Compose on these versions. - Fix:
HighPerformanceConfig.requiresCompatibilityMode()now returnstrueforSDK ≤ 30(Android 11 and below), switching toImplementationMode.COMPATIBLE(TextureView) for correct rendering. - Fix:
setLayerType(LAYER_TYPE_HARDWARE)is no longer applied on Android ≤ 11, eliminating the conflict with TextureView. - Fix: Camera initialization delay now applies to Android 7–11 (was only Android 10), preventing surface-not-ready errors on older devices.
- Fix: Minor stability improvements and dependency updates.
- Updated Kotlin to
2.3.20, Compose Multiplatform to1.10.3, AGP to8.13.2.
- New:
rememberImagePickerKMP(config)— unified Compose state-holder API. ReturnsImagePickerKMPStatewithlaunchCamera(),launchGallery()and reactiveresult. NoRender()or manual booleans needed. - New:
ImagePickerKMPConfig— single configuration object for camera, gallery, crop, UI and permissions. - New:
ImagePickerResult— sealed hierarchy:Idle | Loading | Success | Dismissed | Errorfor exhaustive result handling. - New: Per-launch overrides — override any parameter in
launchCamera()/launchGallery()without mutating the global config. - Fix (Android):
ImagePickerLauncheris now wrapped in a fullscreenDialog— fixes camera not visible when placed outside a container. - The
rememberImagePickerKMP()API is the only public API going forward.
- Breaking:
fileSizenow returns bytes (was KB). Divide by 1024 to get KB. - New: Improved WASM target support with better browser compatibility.
- New:
MimeType.APPLICATION_PDF— select PDFs from gallery. - Fixed crop rotation on iOS in landscape orientation.
- Detekt static analysis integrated into CI pipeline.
- Improved error handling with typed
ImagePickerException.
- New: Circular, square and freeform crop modes with zoom/rotation.
- New: EXIF metadata extraction — GPS, ISO, exposure, camera model.
- Fix: Memory leak on Android when cancelling gallery picker.
- Kotlin 2.3.x required — ABI incompatible with older versions.
- New: Multiple gallery selection with
allowMultiple = true. - New: MIME type filtering with
mimeTypeMismatchMessage. - Android smart picker: images → gallery, PDFs → file explorer, mixed → file explorer.