ImagePickerKMP v1.1.0
GitHub
NEW v1.1.0 — Modular architecture, core module & auto-initialization
Open Source • MIT License

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.

Android API 24+ iOS 12.0+ Desktop JVM JS / Web WASM
rememberImagePickerKMP NEW in v1.0.35
The new recommended Compose API — one call, zero booleans. Returns an ImagePickerKMPState with reactive result, launchCamera() & launchGallery(). No manual state, no showCamera = true toggles ever again.
See the new API

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

RequirementMinimum VersionNotes
Kotlin2.3.20Breaking — ABI incompatible with < 2.3.x
Compose Multiplatform1.10.3Requires Kotlin 2.3.x
Android minSdk24
Android compileSdk36
iOS12.0+
JDK (Desktop)21+
imagepicker-core0.0.1Transitive — included automatically via any module
Kotlin version is mandatory. Projects using Kotlin < 2.3.x will fail with ABI version incompatible. If you need Kotlin 2.1.x, use a previous release.

Installation

Kotlin Multiplatform

build.gradle.kts
// commonMain dependencies — add only what your app uses

// ✅ Published on Maven Central
implementation("io.github.ismoy:imagepickerkmp:1.1.0")         // Photo

// 🔜 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-scanner:??")
// implementation("io.github.ismoy:imagepickerkmp-video-player:??")

React / JavaScript (NPM)

terminal
npm install imagepickerkmp
Only imagepickerkmp and imagepicker-core are currently published on Maven Central. The ecosystem modules (video, audio, scanner, player) are documented here as preview — they will be published as separate artifacts once 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" />
Zero config on Android. The library auto-manages camera and media store permissions internally. No manifest entries are needed. The CAMERA permission is optional — add it only if your app directly accesses the camera outside of this library.

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().

Recommended for all new code. 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:

Kotlin
@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:

Kotlin
// 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

ParameterTypeDefaultDescription
cameraCaptureConfigCameraCaptureConfigdefaultsCamera behaviour: compression, EXIF, crop config
galleryConfigGalleryConfigdefaultsMulti-select, MIME types, selection limit, EXIF, redact GPS
cropConfigCropConfigdefaultsCrop UI — set CropConfig(enabled = true) to activate crop after every capture or selection
permissionAndConfirmationConfigPermissionAndConfirmationConfigdefaultsCustom @Composable permission dialogs & handlers

GalleryConfig — Parameters

ParameterTypeDefaultDescription
allowMultipleBooleanfalseAllow multiple file selection
mimeTypesList<MimeType>[IMAGE_ALL]Allowed file types
selectionLimitInt30Maximum selectable files (requires allowMultiple = true)
includeExifBooleanfalseExtract EXIF metadata from images
redactGpsDataBooleantrueRemove GPS (lat/lon/alt) from EXIF before delivering it
mimeTypeMismatchMessageString?nullCustom message when the file does not match mimeTypes

ImagePickerKMPState — Methods & properties

Method / PropertyDescription
result: ImagePickerResultReactive state. Starts as Idle. Observe with when. Updates automatically.
isCropActive: Booleantrue 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.
There is no 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

StateWhen it occursAvailable data
IdleInitial state, before any action, and after reset()
LoadingThe picker is open and waiting for user selection
SuccessThe user selected / captured images successfullyphotos: List<PhotoResult>, first: PhotoResult?
DismissedThe user closed the picker without selecting anything
ErrorAn error occurred during capture or selectionexception: Exception
Kotlin — Exhaustive state handling
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

Before (legacy)
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")
}
After (recommended)
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

Kotlin
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

Kotlin
val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cameraCaptureConfig = CameraCaptureConfig(
            compressionLevel = CompressionLevel.MEDIUM,
            includeExif = true,
            redactGpsData = false,
            cropConfig = CropConfig(
                enabled = true,
                circularCrop = true,
                squareCrop = true,
                freeformCrop = false
            )
        )
    )
)

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

ParameterTypeDefaultDescription
compressionLevelCompressionLevel?LOWImage compression level (LOW / MEDIUM / HIGH / null)
includeExifBooleanfalseExtract EXIF metadata upon capture
redactGpsDataBooleantrueStrip GPS coordinates from EXIF for privacy
cropConfigCropConfigCropConfig()Crop configuration for camera captures
permissionAndConfirmationConfigPermissionAndConfirmationConfigPermissionAndConfirmationConfig()Permission dialogs & handlers


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.

Kotlin
val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cropConfig = CropConfig(
            enabled = true,
            circularCrop = true,
            squareCrop = true,
            freeformCrop = true
        )
    )
)

// isCropActive = true while the crop UI is shown
if (picker.isCropActive) {
    CircularProgressIndicator()
}

Button(onClick = { picker.launchCamera() }) { Text("Camera + Crop") }
Button(onClick = { picker.launchGallery() }) { Text("Gallery + Crop") }

With rememberImagePickerKMP (crop enabled)

Kotlin
val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cropConfig = CropConfig(enabled = true)
    )
)
picker.launchCamera()
ParameterTypeDefaultDescription
enabledBooleanfalseEnable crop UI after capture
circularCropBooleantrueShow circular crop option
squareCropBooleantrueShow square crop option
freeformCropBooleanfalseShow free-form crop option
Tracking crop state with 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 show a loading indicator or disable UI elements while the crop UI is visible.
if (picker.isCropActive) {
    CircularProgressIndicator() // crop UI is open
}

Image Compression

Automatic background compression with configurable levels. Works for camera and gallery on Android and iOS.

Kotlin
// Camera with compression
val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cameraCaptureConfig = CameraCaptureConfig(
            compressionLevel = CompressionLevel.HIGH
        )
    )
)
picker.launchCamera()

// Gallery with compression
val galleryPicker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cameraCaptureConfig = CameraCaptureConfig(
            compressionLevel = CompressionLevel.MEDIUM
        )
    )
)
galleryPicker.launchGallery()
LevelJPEG QualityMax DimensionUse Case
LOW85%3840 px (4K)Near-lossless, large files
MEDIUM70%1920 px (FHD)Balanced quality/size
HIGH50%1280 px (HD)Maximum size reduction
Default compression is LOW. CameraCaptureConfig uses CompressionLevel.LOW by default (near-lossless, 85% quality). Set to null to disable compression entirely and get the original full-quality image.

EXIF Metadata

Extract rich metadata from photos on Android and iOS. Requires includeExif = true.

Kotlin
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()
FieldTypeDescription
GPS
latitudeDouble?GPS latitude — redacted by default (redactGpsData = true)
longitudeDouble?GPS longitude — redacted by default
altitudeDouble?GPS altitude in meters — redacted by default
Date & Time
dateTakenString?Date and time photo was taken
dateTimeString?General date/time (alias of dateTaken)
digitizedTimeString?Date image was digitized
modifiedTimeString?Last modified date
Camera
cameraModelString?Camera/device model
cameraManufacturerString?Camera manufacturer
softwareString?Processing software
Capture Settings
flashString?Flash status
isoString?ISO sensitivity
apertureString?Aperture f-stop value
shutterSpeedString?Shutter speed (exposure time)
focalLengthString?Focal length in mm
whiteBalanceString?White balance setting
exposureBiasString?Exposure compensation
meteringModeString?Metering mode used
Image Properties
imageWidthInt?Original width in pixels
imageHeightInt?Original height in pixels
orientationString?Image rotation/orientation
colorSpaceString?Color space (sRGB, Adobe RGB, etc.)
thumbnailString?Base64 thumbnail (~5–20 KB). Avoid caching.
GPS is redacted by default. 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.

Kotlin
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 -> {}
}
ExtensionReturn TypeDescription
photo.loadPainter()Painter?Decodes image into Compose Painter for Image() composables
photo.loadImageBitmap()ImageBitmap?Decodes image into Compose ImageBitmap for Canvas rendering
photo.loadBytes()ByteArrayReads complete file content into a byte array
photo.loadBase64()StringEncodes image data to Base64 string for REST API payloads
photo.absolutePathStringReturns direct platform file path (resolves content:// and file://)
photo.asPath()PathConverts absolutePath into a kotlinx.io.files.Path
photo.exists()BooleanVerifies if the file exists on the local file system
photo.asSource()SourceOpens a buffered kotlinx.io.Source for high-performance reading
photo.asRawSource()RawSourceOpens an unbuffered kotlinx.io.RawSource for low-level file reading
photo.transferToSink(sink)UnitStreams 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.

ValueMIME StringDescription
MimeType.IMAGE_ALLimage/*All image formats (default)
MimeType.IMAGE_JPEGimage/jpegJPEG images
MimeType.IMAGE_PNGimage/pngPNG images
MimeType.IMAGE_WEBPimage/webpWebP images
MimeType.IMAGE_GIFimage/gifGIF images
MimeType.IMAGE_BMPimage/bmpBMP images
MimeType.IMAGE_HEICimage/heicHEIC — iOS native format
MimeType.IMAGE_HEIFimage/heifHEIF — iOS native format
MimeType.APPLICATION_PDFapplication/pdfPDF documents

Utility Methods

Kotlin
// 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.

PropertyTypeDefaultDescription
customPermissionHandler((PermissionConfig) -> Unit)?nullCustom permission check/request callback receiving localized PermissionConfig
customDeniedDialog@Composable ((onRetry: ()->Unit, onDismiss: ()->Unit) -> Unit)?nullCustom Composable dialog shown when permission is denied
customSettingsDialog@Composable ((onOpenSettings: ()->Unit, onDismiss: ()->Unit) -> Unit)?nullCustom Composable dialog shown when permission is permanently denied
cancelButtonTextIOSString?nulliOS permission alert cancel button label override
onCancelPermissionConfigIOS(() -> Unit)?nulliOS 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.

Kotlin
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.

Kotlin
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.

Kotlin
// Create localized PermissionConfig using i18nKonfig strings
val config: PermissionConfig = PermissionConfig.createLocalizedComposable()

// Custom PermissionConfig
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"
)
PhotoResult fields: use result.uri (not .image), result.fileSize in bytes (not KB), result.mimeType (not .format), result.width, result.height.

Gallery Options — GalleryConfig

Kotlin
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") }
ParameterTypeDefaultDescription
allowMultipleBooleanfalseEnable multi-file selection
mimeTypesList<MimeType>[IMAGE_ALL]Allowed file types
selectionLimitInt30Max items when allowMultiple (iOS)
includeExifBooleanfalseExtract EXIF metadata
redactGpsDataBooleantrueStrip 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
npm install imagepickerkmp

React Component

TypeScript / React
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.

Full React guide available. See the React Integration Guide for complete examples with Next.js, Vite and plain React.

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.

build.gradle.kts
implementation("io.github.ismoy:imagepickerkmp-video:TBD" // coming soon — not yet published)

iOS — Info.plist

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

Kotlin
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 -> {}
}
StateWhen
IdleInitial or after reset()
LoadingPicker is open
Success(video)Video selected / recorded — contains VideoResult
CancelledUser 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.

build.gradle.kts
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.

Kotlin
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

Kotlin
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 LayoutBest for
AudioPlayerLayout.VoiceMessageChat apps, read/unread status
AudioPlayerLayout.CompactToolbars, minimal space
AudioPlayerLayout.MusicFull player with cover art
AudioPlayerLayout.PodcastPodcast / audiobook with skip controls

Scanner — imagepickerkmp-scanner Coming Soon

Live barcode and QR code scanning via the camera. Supports 18 barcode formats, batch mode, and static scanning from an image ByteArray.

build.gradle.kts
implementation("io.github.ismoy:imagepickerkmp-scanner:TBD" // coming soon — not yet published)

Basic Usage

Kotlin
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 -> {}
}
Static scanning from gallery: use createStaticCodeScanner().scanImage(bytes) to decode a barcode from a ByteArray without opening the camera. Available on Android and iOS.

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 caseModule
Ready-made voice message / podcast / music UIimagepickerkmp-audioImagePickerAudioPlayer
Custom player UI built from scratchimagepickerkmp-audio-playerrememberAudioPlayerState
build.gradle.kts
implementation("io.github.ismoy:imagepickerkmp-audio-player:TBD" // coming soon — not yet published)

Usage — rememberAudioPlayerState

Kotlin
@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

TypeWhen 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 / PropertyDescription
isPlaying: BooleanWhether audio is currently playing
currentPositionMs: LongCurrent playback position in milliseconds
durationMs: LongTotal duration in milliseconds
play()Start or resume playback
pause()Pause at current position
seekTo(positionMs)Seek to an arbitrary position
You can also use 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.

build.gradle.kts
implementation("io.github.ismoy:imagepickerkmp-video-player:TBD" // coming soon — not yet published)

Basic Usage

Kotlin
// 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 TypeExample
VideoSource.UrlHTTP/HTTPS, HLS .m3u8, DASH .mpd
VideoSource.LocalFile path or Android content:// URI
VideoSource.PlaylistOrdered 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.

Kotlin
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

FeatureAndroidiOSDesktopJS/WebWASM
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 — coming soon
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 / PropertyDescription
result: ImagePickerResultReactive picker state: Idle | Loading | Success | Dismissed | Error
isCropActive: Booleantrue 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

ParameterTypeDefaultDescription
cameraCaptureConfigCameraCaptureConfigCameraCaptureConfig()Camera capture, compression & EXIF settings
galleryConfigGalleryConfigGalleryConfig()Gallery selection, MIME filtering & limit settings
cropConfigCropConfigCropConfig()Post-capture & post-selection crop UI settings
permissionAndConfirmationConfigPermissionAndConfirmationConfigPermissionAndConfirmationConfig()Permission handler & custom dialog configurations

CameraCaptureConfig

ParameterTypeDefaultDescription
compressionLevelCompressionLevel?CompressionLevel.LOWImage compression level (LOW / MEDIUM / HIGH / null)
includeExifBooleanfalseExtract EXIF metadata upon capture
redactGpsDataBooleantrueStrip GPS coordinates from EXIF for privacy
permissionAndConfirmationConfigPermissionAndConfirmationConfig…()Custom permission dialogs & handlers
cropConfigCropConfigCropConfig()Crop configuration after camera capture

GalleryConfig

ParameterTypeDefaultDescription
allowMultipleBooleanfalseEnable multi-file selection
mimeTypesList<MimeType>[IMAGE_ALL]Allowed MIME types
selectionLimitInt30Maximum selection limit (iOS)
includeExifBooleanfalseExtract EXIF metadata upon gallery pick
redactGpsDataBooleantrueStrip GPS coordinates from EXIF
mimeTypeMismatchMessageString?nullCustom warning message when unselected MIME type is picked

PermissionAndConfirmationConfig

ParameterTypeDefaultDescription
customPermissionHandler((PermissionConfig) -> Unit)?nullCustom callback to manage permission request flow
customDeniedDialog@Composable ((onRetry: ()->Unit, onDismiss: ()->Unit) -> Unit)?nullCustom dialog when permission is denied
customSettingsDialog@Composable ((onOpenSettings: ()->Unit, onDismiss: ()->Unit) -> Unit)?nullCustom dialog when permission is permanently denied
cancelButtonTextIOSString?nullCustom cancel text for iOS alert dialogs
onCancelPermissionConfigIOS(() -> Unit)?nulliOS callback when permission alert is cancelled

CropConfig

ParameterTypeDefaultDescription
enabledBooleanfalseEnable crop UI after capture or gallery pick
aspectRatioLockedBooleanfalseLock aspect ratio during crop manipulation
circularCropBooleantrueEnable circular crop handle/overlay
squareCropBooleantrueEnable square 1:1 crop preset
freeformCropBooleanfalseEnable freeform ratio cropping

PhotoResult / GalleryPhotoResult

GalleryPhotoResult is a typealias for PhotoResult — both camera and gallery results use the same object model.

Field / PropertyTypeDescription
uriStringPlatform-native URI string of the selected/captured file
fileNameString?File name with extension (e.g., "photo.jpg")
fileSizeLong?File size in bytes (divide by 1024 for KB)
mimeTypeString?MIME type string (e.g. "image/jpeg", "image/png")
widthInt?Image width in pixels
heightInt?Image height in pixels
exifExifData?EXIF metadata — populated when includeExif = true
absolutePathString (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:

ComponentPackageDescription
CoreServicescoreService locator providing access to PermissionManager, FileSystemManager, and MediaLogger
PermissionManagercore.permissionsAbstracts platform permission checks & requests (Camera, Photos)
FileSystemManagercore.filesystemAbstracts file creation, URI resolution, temporary storage, and path mapping
MediaLoggercore.loggerInternal structured console logger with configurable LogLevel
PlatformFilecore.filesystemCross-platform file abstraction wrapper
PlatformUricore.uriCross-platform URI resolution helper
Breaking change v1.0.35: fileSize returns bytes (previously KB). Migrate: val sizeKB = (result.fileSize ?: 0) / 1024.0

ExifData Fields

All fields are nullable. Availability depends on the device, image origin and whether redactGpsData is false.

FieldTypeDescription
GPS & Location
latitudeDouble?GPS latitude (stripped if redactGpsData = true)
longitudeDouble?GPS longitude
altitudeDouble?GPS altitude in metres
Date & Time
dateTakenString?Original date/time when the photo was taken (yyyy:MM:dd HH:mm:ss)
dateTimeString?General date/time — alias of dateTaken for compatibility
digitizedTimeString?Date image was digitized
originalTimeString?Original creation time
modifiedTimeString?Last modified date
utcTimeString?UTC date and time
Camera
cameraManufacturerString?Camera manufacturer (also exposed as cameraMake)
cameraMakeString?Alias for cameraManufacturer
cameraModelString?Camera / device model name
softwareString?Software used to produce the image
ownerString?Owner / copyright information
focalLengthString?Focal length in mm
apertureString?Aperture (f-number)
Capture Settings
isoString?ISO sensitivity
shutterSpeedString?Shutter speed
exposureBiasString?Exposure compensation (EV)
meteringModeString?Metering mode (multi, spot, etc.)
flashString?Flash fired / not fired
whiteBalanceString?White balance mode
sceneCaptureTypeString?Scene capture type (standard, landscape, portrait, night)
Image Properties
imageWidthInt?Image pixel width
imageHeightInt?Image pixel height
orientationString?EXIF orientation (1–8)
colorSpaceString?Color space (sRGB, Adobe RGB, etc.)
xResolutionString?Horizontal resolution (DPI)
yResolutionString?Vertical resolution (DPI)
resolutionUnitString?Resolution unit (inches / cm)
compressionString?Compression method
thumbnailString?Base64 encoded thumbnail data (~5–20 KB). Avoid caching.

Changelog

Recent releases and what changed in each version. Full history on GitHub Releases.

Coming Soon New August 2026
  • New: imagepickerkmp-video — coming soon — video recording and gallery video picking. rememberVideoPicker(), VideoPickerConfig, quality presets (360p–1080p), VideoResult with codec info, validation constraints.
  • New: imagepickerkmp-audio — coming soon — audio recording with real-time waveform. Two APIs: inline AudioRecorder widget (hold-to-record / tap-to-record) and modal rememberAudioPicker(). Includes built-in AudioPlayer with four layouts (VoiceMessage, Compact, Music, Podcast).
  • New: imagepickerkmp-audio-player — coming soon — low-level audio playback engine. rememberAudioPlayerManager() with full PlaybackState (position, duration, speed) for custom player UIs.
  • New: imagepickerkmp-scanner — coming soon — live barcode and QR code scanning. rememberScannerPicker(), 18 barcode formats, batch mode, static image scanning, custom layout support.
  • 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.
v1.1.0 New Breaking July 2026
  • Breaking: Project refactored into a modular ecosystem. The library is now split into imagepicker-core (shared infrastructure) and imagepickerkmp-photo (photo capture, gallery, crop, EXIF, compression). The dependency artifact io.github.ismoy:imagepickerkmp continues to work seamlessly.
  • New: imagepicker-core module — shared permissions, filesystem, URI handling, and logging across modules.
  • New: Auto-initialization on Android via ContentProvider (CoreInitializer) — manual initialization no longer required.
  • New: Internationalization support via i18nKonfig in the core module.
  • New: Tag-based auto-release CI workflow for streamlined automated publishing.
v1.0.43 Fix June 2026
  • 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 via CGImageSource.
  • Fix (iOS): Resolved a false-positive dismissal during the presentation animation that could freeze or cancel the picker.
  • Fix (Android) #136: PhotoResult.absolutePath no longer crashes with FileNotFoundException for cropped images. Raw absolute file paths (e.g. cropped files in cacheDir) are now handled alongside content:// and file:// URIs.
v1.0.42 New Breaking June 2026
  • Breaking: Legacy API removed from public surface. ImagePickerLauncher and GalleryPickerLauncher are now internal — only rememberImagePickerKMP() is the public API.
  • Breaking: ImagePickerConfig is now internal. Use ImagePickerKMPConfig instead.
  • New: Internal functions renamed to PlatformCameraRenderer / PlatformGalleryRenderer for 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: CameraScaleType enum — 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 rememberImagePickerKMP API. Legacy examples removed.
v1.0.40 New April 2026
  • New: PhotoResult.absolutePath extension — 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.
v1.0.39 Fix April 2026
  • Fix (Android 7–11): Camera preview was blank/black on Android 11 (API 30) and below. Root cause: PreviewView was hardcoded to ImplementationMode.PERFORMANCE (SurfaceView), which does not render inside Jetpack Compose on these versions.
  • Fix: HighPerformanceConfig.requiresCompatibilityMode() now returns true for SDK ≤ 30 (Android 11 and below), switching to ImplementationMode.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.
v1.0.38 Fix April 2026
  • Fix: Minor stability improvements and dependency updates.
  • Updated Kotlin to 2.3.20, Compose Multiplatform to 1.10.3, AGP to 8.13.2.
v1.0.37 New March 2026
  • New: rememberImagePickerKMP(config) — unified Compose state-holder API. Returns ImagePickerKMPState with launchCamera(), launchGallery() and reactive result. No Render() or manual booleans needed.
  • New: ImagePickerKMPConfig — single configuration object for camera, gallery, crop, UI and permissions.
  • New: ImagePickerResult — sealed hierarchy: Idle | Loading | Success | Dismissed | Error for exhaustive result handling.
  • New: Per-launch overrides — override any parameter in launchCamera() / launchGallery() without mutating the global config.
  • Fix (Android): ImagePickerLauncher is now wrapped in a fullscreen Dialog — fixes camera not visible when placed outside a container.
  • The rememberImagePickerKMP() API is the only public API going forward.
v1.0.35 Breaking New March 2026
  • Breaking: fileSize now 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.
v1.0.34 New February 2026
  • Improved error handling with typed ImagePickerException.
v1.0.32 New Fix January 2026
  • 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.
v1.0.28 New December 2025
  • New: Multiple gallery selection with allowMultiple = true.
  • New: MIME type filtering with mimeTypeMismatchMessage.
  • Android smart picker: images → gallery, PDFs → file explorer, mixed → file explorer.
View full changelog on GitHub