1

I have Rect object with coordinates of an exact part of image I want to display in Image composable. Black box is part I want to be displayed:

enter image description here

e.g. this box is left 250, top 10, width 80, height 150. I want to show exactly this piece of image.

I tried with .offset and .align modifiers, but never succeded:

val box = ...
val dens = LocalDensity.current
val (x,y) = calculateOffset(box)
        Box(
            modifier = Modifier
                .width(200.dp)
                .height(200.dp)
        ) {
            Image(
                painter = rememberAsyncImagePainter(img_path),
                modifier = Modifier.align { _, _, _ ->
                        IntOffset(x, y)
                    }
                    .scale(calculateScale()),
                contentScale = ContentScale.None,
            )

Do you know any possible solutions how to display only exact part of the image?

upd: I am looking for some smart solution with composable Modifiers.

StayCool
  • 421
  • 1
  • 9
  • 23
  • What does this `calculateScale()` do? Isn't it how you scale from physical image size to Composable size? – Thracian Aug 19 '23 at 13:21

1 Answers1

1

Edit

If you wish to turn this into a Modifier you can do it as

fun Modifier.drawImageSection(
    rect: Rect,
    painter: Painter
) = this.then(
    Modifier.drawWithCache {

        val painterSize = painter.intrinsicSize

        if (painterSize != androidx.compose.ui.geometry.Size.Unspecified){

            val topLeft = Offset(
                x = rect.topLeft.x.coerceIn(0f, painterSize.width),
                y = rect.topLeft.y.coerceIn(0f, painterSize.height)
            )

            val updatedRect = Rect(
                offset = topLeft,
                size = androidx.compose.ui.geometry.Size(
                    width = (rect.width ).coerceIn(0f, painterSize.width- topLeft.x),
                    height = (rect.height ).coerceIn(0f, painterSize.height- topLeft.y)
                ),
            )

            val scaleX = size.width / painterSize.width
            val scaleY = size.height / painterSize.height

            // We scale from Bitmap dimension to Composable dimension
            val left = updatedRect.left * scaleX
            val top = updatedRect.top * scaleY
            val right = updatedRect.right * scaleX
            val bottom = updatedRect.bottom * scaleY

            onDrawWithContent {
                clipRect(
                left = left,
                top = top,
                right = right,
                bottom = bottom
                ) {

                    with(painter) {
                        draw(size)
                    }
                }
            }
        }else {
            onDrawWithContent {}
        }

    }
)

Usage

Box(
    modifier = Modifier
        .border(2.dp, Color.Green)
        .size(200.dp)
        .drawImageSection(
        rect = Rect(
            offset = Offset(170f,18f),
            size = androidx.compose.ui.geometry.Size(205f, 358f)
        ),
        painter = painter
    )
)

Result

enter image description here

Because it's a picture of original image with incorrect size you won't be able to get correct results.

The image you posted is 417x419px, results i get measuring with a tool for the image you shared are

// These are the dimensions on real image you want to clip at
val left = 170f
val top = 18f
val right = (170f + 205f)
val bottom = (18f + 358f)

Using this information first you need to get dimensions of image. It can be retrieved using painter.intrinsicSize.

Then you need to get scale for width and height for Compsosable dimensions(200.dp in pixel) versus image dimensions.

After getting these, since scaling you can clip inside a rect

enter image description here

    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
            .data(R.drawable.test)
            .size(Size.ORIGINAL) // Set the target size to load the image at.
            .build()
    )

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp)
    ) {

        Text(text = "painter size: ${painter.intrinsicSize}")

        Image(
            modifier = Modifier
                .border(2.dp, Color.Red)
                .drawWithContent {

                    val painterSize = painter.intrinsicSize
                    if (painterSize != androidx.compose.ui.geometry.Size.Unspecified &&
                        painterSize.width != 0f &&
                        painterSize.height != 0f
                    ) {
                        val scaleX = size.width / painterSize.width
                        val scaleY = size.height / painterSize.height
                        // We scale from Bitmap dimension to Composable dimension
                        val left = 170f * scaleX
                        val top = 18f * scaleY
                        val right = (170f + 205f) * scaleX
                        val bottom = (18f + 358f) * scaleY
                        clipRect(
                            left = left,
                            top = top,
                            right = right,
                            bottom = bottom
                        ) {
                            this@drawWithContent.drawContent()
                        }
                    }
                },
            painter = painter,
            contentDescription = null,
            contentScale = ContentScale.FillBounds
        )
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • The image I provided is just a visualization to clearly understand the question. I tried to test your composable, but it's no work. – StayCool Aug 18 '23 at 20:42
  • How does it not work? I posted the image from the code above using image you posted. The values i set are also arbitrary to match rectangle position of your image – Thracian Aug 18 '23 at 21:02
  • Pardon me, it didn't worked in @Preview. – StayCool Aug 18 '23 at 21:14
  • Will try to adapt your solution to actually show only desired box, and drop everything else. Thanks! – StayCool Aug 18 '23 at 21:22
  • 1
    If you wish to test it out download the image you posted paste my code inside a Composable and add any top, left, right, bottom for a rectangle in dimensions of image in pixel. If image is 4000x3000px and you wish to display (1000,1000,2000,2000) in 200.dp Composable you just set these values and that section of Image fit inside 200.dp will be displayed only. There are 4 steps required. 1-) get scale between Bitmap and Composable. 2-) Draw your Bitmap in Image Composable with ContentScale, 3-) Scale rect from Bitmap dimensions to Composable dimensions 4-) clip Image with clipRect function – Thracian Aug 18 '23 at 21:23
  • I mean painter by Bitmap but what actually mean is the dimensions of physical file. You can also do with Canvas by using ImageBitmap instead of Painter too. drawImage() has dstSize and srcSize params to scale from physical file to Canvas dimensions. Then by using clip rect with scale you can get same result as answer above – Thracian Aug 18 '23 at 21:26
  • Kinda confused with your four steps (it's too late rn). Let me check it in the morning and I will be back with feedback. Thanks for the manual p.s. yeah I tested it earlier when wrote "Pardon Me" – StayCool Aug 18 '23 at 21:30
  • You can check out updated answer with a Modifier only – Thracian Aug 19 '23 at 12:51
  • I don't wish to turn your solution to Modifier just because, I wanted modifier because I thought that there are just simple solution somewhere. E.g. Maybe it's possible to clip image to a desired rect, and then offset and scale image in a such manner that it will be only desired part visible. – StayCool Aug 19 '23 at 13:05
  • And yeah I didn't understood yet how exactly do I show only cropped part, without frame of original image size – StayCool Aug 19 '23 at 13:06
  • If you wish to crop based on Container on Composable size you can without knowing physical file size but in your question you ask for `left 250, top 10, width 80, height 150` these are pixels of image i assume. And other option is you can clip with a Shape in similar way. But your question and what you really ask for is very vauge. If you wish to draw in Composable bounds then you don't need image dimensions. – Thracian Aug 19 '23 at 13:14
  • And other way is to create a shape and clip with it as in this answer. https://stackoverflow.com/questions/76849407/remove-default-elevation-from-image-in-jetpack-compose/76851388#76851388. But you should decide if you wish to crop within image dimensions or Composable dimensions. If you wish to crop 4000x4000 image that fits 200.dp at (1000,1000,2000,2000) then you need to know size of image – Thracian Aug 19 '23 at 13:16
  • If you wish to clip an image no matter what size it is at 50.dp, 50.dp, 100.dp, 100.dp then you don't need to know image size but this will change cropped area based on what size your image has. – Thracian Aug 19 '23 at 13:17
  • ` this box is left 250, top 10, width 80, height 150. I want to show exactly this piece of image.` if this is position inside image then you need to convert this to Composable plane. – Thracian Aug 19 '23 at 13:22
  • 1. I know the size of the image, I know the size of desired crop area, I know all sizes 2. I asked for *left 250, top 10, width 80, height 150* because I will provide such values to function that will crop the original image to this small rectangle. 3. Yes, I wish to draw in composable bounds, and I want them to be *left 250, top 10, width 80, height 150* of the original image. I am sorry I confused you so much, I really tried to formulate the question in a clear manner – StayCool Aug 19 '23 at 13:23
  • what is "convert to a composable plane"? – StayCool Aug 19 '23 at 13:23
  • https://stackoverflow.com/questions/76849407/remove-default-elevation-from-image-in-jetpack-compose/76851388#76851388 I will try to use custom shape thx – StayCool Aug 19 '23 at 13:25
  • Let's say you have 1000x1000x px image, and you want to draw it inside 100x100px composable, for simplicity let's say 100.dp size. If you crop at position of 40px when you display it in smaller image it should be 4px position. Just remove the scaleX and scaleY and see yourself. Only thing that matters in my answer is scale. updatedRectangle is for limiting original rectangle in physical file so you don't get crashes, i could have omitted that – Thracian Aug 19 '23 at 13:29
  • Even if you use shape option you still need the Rectangle part. If you check out my option you posted above i use the same Rect in both clip and draw modifier. It's same for your question to. You need to get correct rectangle to clip either with shape or clipRect functions. – Thracian Aug 19 '23 at 13:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254968/discussion-between-thracian-and-staycool). – Thracian Aug 19 '23 at 13:30
  • And the thing i don't get i posted an answer that does not require add a fixed image size that you should enter instead gets it from painter itself and all you have to do is pass draw area as a rect and painter that's all. Why do you insist on adding size yourself. And if you do just pass it and replace the part i get painterSize – Thracian Aug 19 '23 at 13:34
  • Let's say you have a 4000x4000x px file you draw inside a 1000x1000px Image composable when you draw something at (3000, 3000) in Canvas or on physical file you need to scale it back to (750, 750) to draw on Image Composable, right? And when you draw something using touch you draw it on Composable dimensions, then you need to convert these to file dimensions. A shape at (500, 500) on Composable translates to (2000,2000) for file. – Thracian Aug 21 '23 at 06:03