
Tool for cropping and transforming images with actions like rotate and flip, utilizing `ImageBitmap`, `Uri`, `File`, or `Painter` as inputs.
With this tool you can crop images in Compose Multiplatform.
| Target | Implemented | Tested |
|---|---|---|
| Android | ☑ | ☑ |
| iOS | ☑ | ☑ |
| JVM Desktop | ☑ | ☑ |
| JS | ☑ | ☑ |
| WasmJS | ☑ | ☑ |
In your shared module's build.gradle.kts add:
kotlin.sourceSets.commonMain.dependencies {
implementation("tech.ryadom:origami:2.0.0")
}Create the state holder with rememberOrigami, hand it to OrigamiImage, and call crop()
when you want the result.
val origami = rememberOrigami(imageBitmap = myBitmap)
OrigamiImage(origami = origami)
// Returns cropped image
origami.crop()rememberOrigami keeps the selection across recompositions and across configuration changes
and process death — the crop area is stored in resolution independent coordinates, so it comes
back in the right place even if the window changed shape. Pass the bitmap back in yourself; it is
not saved.
You can also build one by hand, which is what you want for a custom OrigamiSource:
val source = createYourSource()
val colors = OrigamiColors.createDefault(guidelinesColor = Color.White)
val cropArea = OrigamiCropArea()
val aspectRatio = OrigamiAspectRatio()
val origami = remember { Origami(source, colors, cropArea, aspectRatio) }Always keep the instance in
remember. Constructing anOrigamiinside a composable body throws away the user's selection on every recomposition and re-runs the source scaling.
origami.rotateClockwise() // quarter turns, lossless
origami.rotateCounterClockwise()
origami.flipHorizontally()
origami.flipVertically()
origami.setAspectRatio(OrigamiAspectRatio.Landscape16x9) // or a preset
origami.aspectRatio = 3f / 2f // or a raw ratio
origami.aspectRatio = null // free form
origami.reset() // back to the initial crop area, no transforms
origami.cropRect // observable selection, follows the user's drag
origami.rotationDegrees // 0, 90, 180 or 270crop(OrigamiCompression) runs off the main thread, honours cancellation, and re-encodes the
result until it fits:
scope.launch {
val result = origami.crop(
OrigamiCompression(maxSize = 500 * 1024)
)
}Compression is a lossy JPEG round trip, so it drops the alpha channel.
With OrigamiColors you can customize backgroundColor, guidelinesColor and edgesColor:
interface OrigamiColors {
val backgroundColor: Color
val guidelinesColor: Color
val edgesColor: Color
}With OrigamiCropArea you can customize the crop area:
2.1. Number and width of guidelines
2.2. Shape edges via OrigamiEdges. You can use the default Circle or Rectangle shape, or
create your own using DrawScope
2.3. The highlighted area of the shape via OrigamiHighlightedShape. You can use Circle,
Rectangle (default) or RoundedRectangle by default or create your own shape
2.4. Crop area initial paddings via OrigamiCropAreaPadding
data class OrigamiCropArea(
val highlightedShape: OrigamiHighlightedShape = OrigamiHighlightedShape.Default,
val edges: OrigamiEdges? = OrigamiEdges.Circle(6.dp),
val guidelinesWidth: Dp = 2.dp,
val guidelinesCount: Int = 2,
val initialPaddings: OrigamiCropAreaPadding = OrigamiCropAreaPadding.createDefault()
)2.5. Minimum crop size and how large the corner handles are to the touch, via minSize
and handleTouchTarget. Both are Dp, so they hold up across densities.
data class OrigamiCropArea(
val highlightedShape: OrigamiHighlightedShape = OrigamiHighlightedShape.Default,
val edges: OrigamiEdges? = OrigamiEdges.Circle(6.dp),
val guidelinesWidth: Dp = 2.dp,
val guidelinesCount: Int = 2,
val initialPaddings: OrigamiCropAreaPadding = OrigamiCropAreaPadding.createDefault(),
val minSize: Dp = 56.dp,
val handleTouchTarget: Dp = 32.dp
)With OrigamiAspectRatio you can specify any width / height ratio of crop area you need.
data class OrigamiAspectRatio(
val isVariable: Boolean = false,
val aspectRatio: Float = 1f
)Presets cover the usual cases:
OrigamiAspectRatio.Free // user reshapes freely, starts covering the whole image
OrigamiAspectRatio.Square // 1:1
OrigamiAspectRatio.Landscape4x3
OrigamiAspectRatio.Portrait3x4
OrigamiAspectRatio.Landscape16x9
OrigamiAspectRatio.Portrait9x16
OrigamiAspectRatio.of(width = 3, height = 2) Copyright 2025 Ryadom Tech
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
If you find a bug or want to contribute an improvement, please create an Issue or email opensource@ryadom.tech. Any support will be appreciated.
With this tool you can crop images in Compose Multiplatform.
| Target | Implemented | Tested |
|---|---|---|
| Android | ☑ | ☑ |
| iOS | ☑ | ☑ |
| JVM Desktop | ☑ | ☑ |
| JS | ☑ | ☑ |
| WasmJS | ☑ | ☑ |
In your shared module's build.gradle.kts add:
kotlin.sourceSets.commonMain.dependencies {
implementation("tech.ryadom:origami:2.0.0")
}Create the state holder with rememberOrigami, hand it to OrigamiImage, and call crop()
when you want the result.
val origami = rememberOrigami(imageBitmap = myBitmap)
OrigamiImage(origami = origami)
// Returns cropped image
origami.crop()rememberOrigami keeps the selection across recompositions and across configuration changes
and process death — the crop area is stored in resolution independent coordinates, so it comes
back in the right place even if the window changed shape. Pass the bitmap back in yourself; it is
not saved.
You can also build one by hand, which is what you want for a custom OrigamiSource:
val source = createYourSource()
val colors = OrigamiColors.createDefault(guidelinesColor = Color.White)
val cropArea = OrigamiCropArea()
val aspectRatio = OrigamiAspectRatio()
val origami = remember { Origami(source, colors, cropArea, aspectRatio) }Always keep the instance in
remember. Constructing anOrigamiinside a composable body throws away the user's selection on every recomposition and re-runs the source scaling.
origami.rotateClockwise() // quarter turns, lossless
origami.rotateCounterClockwise()
origami.flipHorizontally()
origami.flipVertically()
origami.setAspectRatio(OrigamiAspectRatio.Landscape16x9) // or a preset
origami.aspectRatio = 3f / 2f // or a raw ratio
origami.aspectRatio = null // free form
origami.reset() // back to the initial crop area, no transforms
origami.cropRect // observable selection, follows the user's drag
origami.rotationDegrees // 0, 90, 180 or 270crop(OrigamiCompression) runs off the main thread, honours cancellation, and re-encodes the
result until it fits:
scope.launch {
val result = origami.crop(
OrigamiCompression(maxSize = 500 * 1024)
)
}Compression is a lossy JPEG round trip, so it drops the alpha channel.
With OrigamiColors you can customize backgroundColor, guidelinesColor and edgesColor:
interface OrigamiColors {
val backgroundColor: Color
val guidelinesColor: Color
val edgesColor: Color
}With OrigamiCropArea you can customize the crop area:
2.1. Number and width of guidelines
2.2. Shape edges via OrigamiEdges. You can use the default Circle or Rectangle shape, or
create your own using DrawScope
2.3. The highlighted area of the shape via OrigamiHighlightedShape. You can use Circle,
Rectangle (default) or RoundedRectangle by default or create your own shape
2.4. Crop area initial paddings via OrigamiCropAreaPadding
data class OrigamiCropArea(
val highlightedShape: OrigamiHighlightedShape = OrigamiHighlightedShape.Default,
val edges: OrigamiEdges? = OrigamiEdges.Circle(6.dp),
val guidelinesWidth: Dp = 2.dp,
val guidelinesCount: Int = 2,
val initialPaddings: OrigamiCropAreaPadding = OrigamiCropAreaPadding.createDefault()
)2.5. Minimum crop size and how large the corner handles are to the touch, via minSize
and handleTouchTarget. Both are Dp, so they hold up across densities.
data class OrigamiCropArea(
val highlightedShape: OrigamiHighlightedShape = OrigamiHighlightedShape.Default,
val edges: OrigamiEdges? = OrigamiEdges.Circle(6.dp),
val guidelinesWidth: Dp = 2.dp,
val guidelinesCount: Int = 2,
val initialPaddings: OrigamiCropAreaPadding = OrigamiCropAreaPadding.createDefault(),
val minSize: Dp = 56.dp,
val handleTouchTarget: Dp = 32.dp
)With OrigamiAspectRatio you can specify any width / height ratio of crop area you need.
data class OrigamiAspectRatio(
val isVariable: Boolean = false,
val aspectRatio: Float = 1f
)Presets cover the usual cases:
OrigamiAspectRatio.Free // user reshapes freely, starts covering the whole image
OrigamiAspectRatio.Square // 1:1
OrigamiAspectRatio.Landscape4x3
OrigamiAspectRatio.Portrait3x4
OrigamiAspectRatio.Landscape16x9
OrigamiAspectRatio.Portrait9x16
OrigamiAspectRatio.of(width = 3, height = 2) Copyright 2025 Ryadom Tech
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
If you find a bug or want to contribute an improvement, please create an Issue or email opensource@ryadom.tech. Any support will be appreciated.