CmpImageCropView
0.1.0indexedLightweight interactive image-cropping widget with seven aspect-ratio modes, pinch-to-zoom and pan, circular profile mask, customizable handles and guides, and full-resolution output that preserves crop state.
Lightweight interactive image-cropping widget with seven aspect-ratio modes, pinch-to-zoom and pan, circular profile mask, customizable handles and guides, and full-resolution output that preserves crop state.
A customizable image cropping component for Compose Multiplatform.
Android · iOS · Desktop · Web
ImageCrop is a lightweight, fully interactive image-cropping widget for Compose Multiplatform. Drag the crop rectangle, pinch to zoom, choose from seven aspect-ratio presets, and get the result back at full source resolution — all from a single composable that runs unchanged on every supported platform.
| Platform | Demo |
|---|---|
| Android | |
| iOS | |
| Desktop | |
| Web |
Available on Maven Central.
gradle/libs.versions.toml
[versions]
cmpImgCropView = "0.1.0"
[libraries]
cmpImgCropView = { module = "io.github.rroohit:CmpImgCropView", version.ref = "cmpImgCropView" }
build.gradle.kts (shared / commonMain)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.cmpImgCropView)
}
}
}
commonMain.dependencies {
implementation("io.github.rroohit:CmpImgCropView:0.1.0")
}
@Composable
fun MyCropScreen(imageData: ImageData) {
// 1. Hold the crop state (survives rotation/config changes).
val imageCrop = rememberSaveableImageCrop(imageData)
Column {
// 2. Render the interactive crop view.
ImageCropView(
imageCrop = imageCrop,
modifier = Modifier.weight(1f),
cropType = CropType.FREE_STYLE,
enableZoom = ,
)
Button(onClick = {
cropped: ImageData = imageCrop.onCrop()
}) {
Text()
}
}
}
To display any ImageData (including the crop result) in Compose, convert it with toImageBitmap():
Image(bitmap = cropped.toImageBitmap(), contentDescription = null)
ImageDataImageData is the library's platform-agnostic image container (an expect class with an actual implementation per platform). You construct it in each platform's source set from that platform's native image type, then pass it into shared code.
Android (androidMain)
import com.cmp.image.cropview.ImageData
val imageData = ImageData(bitmap) // android.graphics.Bitmap
Desktop / JVM (jvmMain)
import com.cmp.image.cropview.ImageData
val imageData = ImageData(bufferedImage) // java.awt.image.BufferedImage
iOS (iosMain)
import com.cmp.image.cropview.ImageData
// encodedBytes = PNG/JPEG bytes; width/height = pixel dimensions
val imageData = ImageData(encodedBytes, width, height)
Web — JS & WasmJS (webMain)
import com.cmp.image.cropview.ImageData
val imageData = ImageData(width, height, encodedBytes)
ImageCropViewCropTypeCropType.aspectRatio(): Float? returns the numeric ratio, or null for FREE_STYLE, SQUARE, and PROFILE_CIRCLE.
EdgeTypeCIRCULAR — round dots at the corners. SQUARE — square bracket handles.
ImageCrop / OnCrop| Member | Description |
|---|---|
onCrop(cropSourceImage: Boolean = true): ImageData | Performs the crop. true (default) maps the selection back to source pixels for full-resolution output; returns the canvas-scaled (lower quality) crop. |
| Factory | Behaviour |
|---|---|
rememberImageCrop(imageData) | In-process state; not preserved across activity recreation. |
Profile-picture cropper — circular mask, no grid:
ImageCropView(
imageCrop = imageCrop,
cropType = CropType.PROFILE_CIRCLE,
showGuideLines = false,
enableZoom = true,
)
Fixed 16:9 banner crop:
ImageCropView(
imageCrop = imageCrop,
cropType = CropType.RATIO_16_9,
)
Custom styling with square handles:
ImageCropView(
imageCrop = imageCrop,
edgeType = EdgeType.SQUARE,
guideLineColor = Color.White,
guideLineWidth = 1.5.dp,
)
Collecting multiple crops:
var results by remember { mutableStateOf<List<ImageData>>(emptyList()) }
ImageCropView(imageCrop = imageCrop, enableZoom = true)
Button(onClick = { results = results + imageCrop.onCrop() }) { Text("Crop") }
OutlinedButton(onClick = { imageCrop.resetView() }) { Text("Reset") }
LazyRow {
items(results) { img ->
Image(bitmap = img.toImageBitmap(), contentDescription = null)
}
}
rememberSaveableImageCrop serializes the crop rectangle, zoom level, and pan offset as normalized ratios, so they restore correctly even when the canvas is laid out at a different size after a rotation or other configuration change. Because the factory keys on the imageData instance, loading a different image automatically starts with a fresh crop state.
The repository includes a demo app (:shared) hosted on every platform:
# Android (device/emulator connected)
./gradlew :androidApp:installDebug
# Desktop (JVM)
./gradlew :desktopApp:run
# Web — JavaScript
./gradlew :webApp:jsBrowserDevelopmentRun
# Web — WebAssembly
./gradlew :webApp:wasmJsBrowserDevelopmentRun
For iOS, open the iosApp/ project in Xcode and run it on a simulator or device.
| Dependency | Version |
|---|---|
| Kotlin | 2.3.21 |
Contributions are welcome. Please open an issue to discuss substantial changes before submitting a pull request at github.com/rroohit/CmpImageCropView.
Copyright 2026 Rohit Chavan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance the License.
You may obtain a copy of the License at
https:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
See LICENSE for the full text.
| Platform | Status | Backing image type |
|---|
| Android | ✅ | android.graphics.Bitmap |
| iOS | ✅ | Encoded bytes (Skia) |
| Desktop (JVM) | ✅ | java.awt.image.BufferedImage |
| Web (JS) | ✅ | Encoded bytes (Skia) |
| Web (WasmJS) | ✅ | Encoded bytes (Skia) |
PROFILE_CIRCLE overlays a circular mask for avatar-style cropping.rememberSaveable.| Parameter | Type | Default | Description |
|---|
imageCrop | ImageCrop | — | State holder from rememberImageCrop / rememberSaveableImageCrop. |
modifier | Modifier | Modifier | Layout modifier for the crop view container. |
guideLineColor | Color | Color(0xFFD1CBE2) | Colour of the border and rule-of-thirds grid. |
guideLineWidth | Dp | 2.dp | Stroke width of the guide lines. |
edgeCircleSize | Dp | 8.dp | Radius of the corner handles (when edgeType = CIRCULAR). |
showGuideLines | Boolean | true | Whether to draw the rule-of-thirds grid. |
cropType | CropType | FREE_STYLE | Aspect-ratio constraint (see below). |
edgeType | EdgeType | CIRCULAR | Corner handle style. |
enableZoom | Boolean | false | Enables pinch-to-zoom and double-tap zoom. |
| Value | Ratio | Notes |
|---|
FREE_STYLE | none | Resize freely to any proportion. |
SQUARE | 1:1 | Locked square. |
PROFILE_CIRCLE | 1:1 | Square selection with a circular overlay mask. |
RATIO_3_2 | 3:2 | Landscape. |
RATIO_4_3 | 4:3 | Standard. |
RATIO_16_9 | 16:9 | Widescreen. |
RATIO_9_16 | 9:16 | Portrait / stories. |
falseresetView() | Resets the crop rectangle and zoom to their initial state. |
rememberSaveableImageCrop(imageData)Crop rect, zoom, and pan survive rotation and config changes. Resets automatically when imageData changes. |
| Compose Multiplatform | 1.11.0 |
Android minSdk | 24 |
Android compileSdk | 36 |
Surfaced from shared tags and platforms — no rankings paid for.