skip to Main Content

How can I only capture the image which is shown inside the box in this camera overlay. I am able to get the bitmap of the image successfully.

enter image description here

I can get the position of the box by this

Image(painter = painterResource(id = R.drawable.ic_background_round),
                contentDescription = null,
                modifier = Modifier.onGloballyPositioned { coordinates ->

                    val rect = coordinates.boundsInRoot()
                    rectTopLeft = rect.topLeft
                    rectBottomRight = rect.bottomRight     
                 
                })

2

Answers


  1. You’ll have to manually crop the Bitmap after capture.

    Bitmap.createBitmap(
      source = originalBitmap,
      x = rect.topLeft.x,
      y = rect.topLeft.y,
      width = newWidth,
      height = newHeight
    )
    
    Login or Signup to reply.
  2. Creating a new bitmap will return a portion of original image in Rectangle shape not in a custom shape. Very likely reason you get that exception is difference between Composable and Bitmap dimensions. When you crop an image on a Composable you need to interpolate left and size of Composable to Bitmap left and size.

    What this means is let’s say your image is 1000x1000px while your screen is 2000x2000px

    if you wish to get (200,200) position with 1000px width/height on Composable you get it as

    val actualLeft =left* (bitmapWidth/composableWidth)
    200*(1000/2000) = 100 should be the coordinate of left position on image. Same goes for y coordinate, width and height.

    What you actually ask is image cropper with shape minus gestures. As i answered in previous question you need to use Canvas. But this time Canvas should be

    androidx.compose.ui.graphics.Canvas
    

    And pass your Image as

        val canvas: androidx.compose.ui.graphics.Canvas = Canvas(imageBitmap)
    

    and

    canvas.apply {
            drawRoundedRect(
               // properties
                paint = paint
            )
        }
    

    then draw the shape, and convert it to a path since we need to use it in this Canvas with a Paint. This Paint should have XferMode.SrcIn. I will update this answer when i’m available. You can check out this question, NativeCanvasSample2is very similar to what you wish to achieve except what i want to achieve is a cropper gestures.

    I made a workaround and sample that how you can clip with a shape SrcIn with compose Canvas or using scaling and interpolation to correctly clip an image using rectDraw and rectCrop.

    @Composable
    fun TransparentClipLayout(
        modifier: Modifier,
        imageBitmap: ImageBitmap,
        width: Dp,
        height: Dp,
        offsetY: Dp,
        crop: Boolean = false,
        onCropSuccess: (ImageBitmap) -> Unit
    ) {
    
        val offsetInPx: Float
        val widthInPx: Float
        val heightInPx: Float
    
        with(LocalDensity.current) {
            offsetInPx = offsetY.toPx()
            widthInPx = width.toPx()
            heightInPx = height.toPx()
        }
    
        BoxWithConstraints(modifier) {
    
            val composableWidth = constraints.maxWidth
            val composableHeight = constraints.maxHeight
    
    
            val widthRatio = imageBitmap.width / composableWidth.toFloat()
            val heightRatio = imageBitmap.height / composableHeight.toFloat()
    
            val rectDraw = remember {
                Rect(
                    offset = Offset(
                        x = (composableWidth - widthInPx) / 2f,
                        y = offsetInPx
                    ),
                    size = Size(widthInPx, heightInPx)
                )
            }
    
            val rectCrop by remember {
                mutableStateOf(
                    IntRect(
                        offset = IntOffset(
                            (rectDraw.left * widthRatio).toInt(),
                            (rectDraw.top * heightRatio).toInt()
                        ),
                        size = IntSize(
                            (rectDraw.width * widthRatio).toInt(),
                            (rectDraw.height * heightRatio).toInt()
                        )
                    )
                )
            }
    
            LaunchedEffect(crop) {
                if (crop) {
                    delay(500)
                    val croppedBitmap = Bitmap.createBitmap(
                        imageBitmap.asAndroidBitmap(),
                        rectCrop.left,
                        rectCrop.top,
                        rectCrop.width,
                        rectCrop.height
                    ).asImageBitmap()
    
                    onCropSuccess(croppedBitmap)
                }
            }
    
            Canvas(modifier = Modifier.fillMaxSize()) {
    
                if (!crop) {
                    drawImage(
                        image = imageBitmap,
                        dstSize = IntSize(size.width.toInt(), size.height.toInt())
                    )
                }
    
                with(drawContext.canvas.nativeCanvas) {
                    val checkPoint = saveLayer(null, null)
    
                    if (!crop) {
                        // Destination
                        drawRect(Color(0x77000000))
    
                        // Source
                        drawRoundRect(
                            topLeft = rectDraw.topLeft,
                            size = rectDraw.size,
                            cornerRadius = CornerRadius(30f, 30f),
                            color = Color.Transparent,
                            blendMode = BlendMode.Clear
                        )
    
                    } else {
                        // Destination
                        drawRoundRect(
                            topLeft = rectDraw.topLeft,
                            size = rectDraw.size,
                            cornerRadius = CornerRadius(30f, 30f),
                            color = Color.Red,
                        )
    
                        // Source
                        drawImage(
                            image = imageBitmap,
                            dstSize = IntSize(size.width.toInt(), size.height.toInt()),
                            blendMode = BlendMode.SrcIn
                        )
                    }
    
                    restoreToCount(checkPoint)
                }
            }
        }
    }
    

    Usage

    var croppedImage by remember { mutableStateOf<ImageBitmap?>(null) }
    
    var crop by remember { mutableStateOf(false) }
    var showDialog by remember { mutableStateOf(false) }
    
    val imageBitmap = ImageBitmap.imageResource(id = R.drawable.landscape1)
    
    Box(modifier = Modifier.fillMaxSize()) {
        TransparentClipLayout(
            modifier = Modifier.fillMaxSize(),
            imageBitmap = imageBitmap,
            width = 300.dp,
            height = 200.dp,
            offsetY = 150.dp,
            crop = crop,
            onCropSuccess = {
                croppedImage = it
                crop = false
                showDialog = true
            }
        )
    
        Button(
            modifier = Modifier
                .padding(20.dp)
                .fillMaxWidth()
                .align(Alignment.BottomStart),
            onClick = { crop = true }) {
            Text("Capture")
        }
    
        if (showDialog) {
            croppedImage?.let {
                ShowCroppedImageDialog(
                    imageBitmap = it
                ) {
                    showDialog = !showDialog
                    croppedImage = null
                }
            }
        }
    }
    

    Dialog

    @Composable
    fun ShowCroppedImageDialog(imageBitmap: ImageBitmap, onDismissRequest: () -> Unit) {
        androidx.compose.material3.AlertDialog(
            onDismissRequest = onDismissRequest,
            text = {
                Image(
                    modifier = Modifier.fillMaxWidth().aspectRatio(),
                    contentScale = ContentScale.Fit,
                    bitmap = imageBitmap,
                    contentDescription = "result"
                )
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        onDismissRequest()
                    }
                ) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        onDismissRequest()
                    }
                ) {
                    Text("Dismiss")
                }
            }
        )
    }
    

    Result

    enter image description here

    In result the one in screen is due to BlendMode.SrcIn if you fusion with the Canvas that takes image you will be able to get it. The one in dialog is the actual one because of cropping image. As i mentioned above you can crop images with a Rectangle.

    You need to implement Canvas(bitmap) for custom shapes as in my question, i might update this answer with androidx.compose.ui.graphics.Canvas = Canvas(imageBitmap) when i’m available in the future, i faced some issues but will fix them soon. This is basically how to build an image cropper with shape and crop after scaling in Bitmap dimensions.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search