Android Bitmap save without transparent area
Asked Answered
G

5

14

I want to save bitmap without transparent area.
Bitmap has large transparent pixel.
So i want to remove that.
How can i do this?

I cant add picture so explain with symbols.
I dont want to crop function.
I hope use filter

┌────────────────────────┐
│  transparent area      │
│   ┌─────────┐          │
│   │crop this│          │
│   └─────────┘          │
└────────────────────────┘
Glynnis answered 3/1, 2015 at 9:58 Comment(3)
you want to fill the transparent area with color?Reinforcement
No i want crop bitmap without transparent area using filter.Glynnis
try this answer: https://mcmap.net/q/194247/-android-canvas-how-do-i-clear-delete-contents-of-a-canvas-bitmaps-living-in-a-surfaceviewReinforcement
H
28

To find the non-transparent area of your bitmap, iterate across the bitmap in x and y and find the min and max of the non-transparent region. Then crop the bitmap to those co-ordinates.

Bitmap CropBitmapTransparency(Bitmap sourceBitmap)
{
    int minX = sourceBitmap.getWidth();
    int minY = sourceBitmap.getHeight();
    int maxX = -1;
    int maxY = -1;
    for(int y = 0; y < sourceBitmap.getHeight(); y++)
    {
        for(int x = 0; x < sourceBitmap.getWidth(); x++)
        {
            int alpha = (sourceBitmap.getPixel(x, y) >> 24) & 255;
            if(alpha > 0)   // pixel is not 100% transparent
            {
                if(x < minX)
                    minX = x;
                if(x > maxX)
                    maxX = x;
                if(y < minY)
                    minY = y;
                if(y > maxY)
                    maxY = y;
            }
        }
    }
    if((maxX < minX) || (maxY < minY))
        return null; // Bitmap is entirely transparent

    // crop bitmap to non-transparent area and return:
    return Bitmap.createBitmap(sourceBitmap, minX, minY, (maxX - minX) + 1, (maxY - minY) + 1);
}
Humpage answered 3/1, 2015 at 10:50 Comment(7)
superb work dear ! just finding about this from last couple of days ! thank you again !Generalissimo
This worked really great. But it took a while to execute. Is it the fastest way to do this job?Hanukkah
@Hanukkah if you call getPixels() or copyPixelsToBuffer() on the bitmap then you can iterate across the pixel data without having to call getPixel() once for each pixel. That should speed it up, but it will complicate things as you will have to take into account the stride and do the pixel indexing yourself. Doing it in native code would probably be faster still but seems needlessly complicated.Humpage
@Hanukkah Changing the algorithm would also speed it up. As it is it does a lot of unnecessary checks. For example you could scan down from the top and then up from the bottom to find the min and max y, then in the remaining region scan across each row, but in each row only check the region less than the current min x and greater than the current max x to work out the min and max x. Performance might be affected by reading the memory out of order however.Humpage
very good answer,,,,, but this process is so slow when i use images to recyclerview.......... can anyone have any code of this which is not slowNeuberger
very much slow process.. make it fastNeuberger
Perfect solution..! :)Bondstone
C
8

Crop transparent border with this github.

public static Bitmap crop(Bitmap bitmap) {

    int height = bitmap.getHeight();
    int width = bitmap.getWidth();
    int[] empty = new int[width];
    int[] buffer = new int[width];
    Arrays.fill(empty, 0);
    int top = 0;
    int left = 0;
    int bottom = height;
    int right = width;

    for (int y = 0; y < height; y++) {
        bitmap.getPixels(buffer, 0, width, 0, y, width, 1);
        if (!Arrays.equals(empty, buffer)) {
            top = y;
            break;
        }
    }

    for (int y = height - 1; y > top; y--) {
        bitmap.getPixels(buffer, 0, width, 0, y, width, 1);
        if (!Arrays.equals(empty, buffer)) {
            bottom = y;
            break;
        }
    }

    empty = new int[height];
    buffer = new int[height];
    Arrays.fill(empty, 0);

    for (int x = 0; x < width; x++) {
        bitmap.getPixels(buffer, 0, 1, x, 0, 1, height);
        if (!Arrays.equals(empty, buffer)) {
            left = x;
            break;
        }
    }

    for (int x = width - 1; x > left; x--) {
        bitmap.getPixels(buffer, 0, 1, x, 0, 1, height);
        if (!Arrays.equals(empty, buffer)) {
            right = x;
            break;
        }
    }

    return Bitmap.createBitmap(bitmap, left, top, right - left + 1, bottom - top + 1);
}
Conaway answered 12/9, 2016 at 14:40 Comment(3)
Could you maybe add the code snippet here, so that other people don't have to go to the github repo?Decommission
Done ;) @MartinGottweis.This code not read all pixels, just slice vertical and horizontal matrix, so it was faster in my tests.Imf
crashes on this line ................................ bitmap.getPixels(buffer, 0, 1, x, top + 1, 1, bufferSize); /////////////////////// java.lang.IllegalArgumentException: y + height must be <= bitmap.height()Neuberger
M
8

I took @Alvaro Menezes's answer and improved it as a Kotlin extension function. I tweaked it a bit, changed some variable names for better readability and it adds more fixes to the issue mentioned by @Ahamadullah Saikat that throws an IllegalArgumentException

Note that reading pixels by line improve a lot the performances against reading this independently as the accepted answer suggest.

/**
 * Trims a bitmap borders of a given color.
 *
 */
fun Bitmap.trim(@ColorInt color: Int = Color.TRANSPARENT): Bitmap {

    var top = height
    var bottom = 0
    var right = width
    var left = 0

    var colored = IntArray(width, { color })
    var buffer = IntArray(width)

    for (y in bottom until top) {
        getPixels(buffer, 0, width, 0, y, width, 1)
        if (!Arrays.equals(colored, buffer)) {
            bottom = y
            break
        }
    }

    for (y in top - 1 downTo bottom) {
        getPixels(buffer, 0, width, 0, y, width, 1)
        if (!Arrays.equals(colored, buffer)) {
            top = y
            break
        }
    }

    val heightRemaining = top - bottom
    colored = IntArray(heightRemaining, { color })
    buffer = IntArray(heightRemaining)

    for (x in left until right) {
        getPixels(buffer, 0, 1, x, bottom, 1, heightRemaining)
        if (!Arrays.equals(colored, buffer)) {
            left = x
            break
        }
    }

    for (x in right - 1 downTo left) {
        getPixels(buffer, 0, 1, x, bottom, 1, heightRemaining)
        if (!Arrays.equals(colored, buffer)) {
            right = x
            break
        }
    }
    return Bitmap.createBitmap(this, left, bottom, right - left, top - bottom)
}
Mcdevitt answered 14/3, 2018 at 15:21 Comment(4)
Something is weird in this code. I think you've confused between "top" and "bottom", because "top" is supposed to be row 0.... Also, shouldn't the "heightRemaining" have +1 added to it? If the image is filled, 100x100, it means it would be equal to top-1-bottom=height-1-0. That's instead of height.Ancylostomiasis
Not only that, but even on the last part, when you create the bitmap, you use right - left for the width, so as right becomes width-1, and left is 0, it means the width shrinks... Same issue for the height.Ancylostomiasis
I believe you are right. feel free to apply your suggestions into the code!Mcdevitt
I created an answer in Kotlin, gathering 2 possible solutions with fixes that I've fixed of them: https://mcmap.net/q/792702/-android-bitmap-save-without-transparent-areaAncylostomiasis
S
0

Following the official doc:

The new bitmap may be the same object as source, or a copy may have been made.

You should take into account when you execute .recycle() with the source bitmap.

Snowinsummer answered 29/3, 2020 at 9:32 Comment(0)
A
0

I took what others have created here, and I think this one solves the end-cases that weren't handled, such as all pixels are of the same given color, and not needing to create a new array of same values either:

/**removes rows/columns of the same color from the edges
 * @return a Bitmap after the removal, or null if completely of the same color*/
fun cropToRemoveSameColoredEdges(bitmap: Bitmap, @ColorInt color: Int = Color.TRANSPARENT): Bitmap? {
    val height = bitmap.height
    val width = bitmap.width
    //this is to handle colors that have various values, but are still just same 100% transparent, so it's the same color
    val checkFullTransparency = Color.alpha(color) == 0
    //handle top&bottom
    //top
    var top = 0
    var buffer = IntArray(width)
    while (top < height) {
        bitmap.getPixels(buffer, 0, width, 0, top, width, 1)
        val firstIndexOfDifferentlyColoredPixel: Int = buffer.indexOfFirst { pixel ->
            if (checkFullTransparency)
                Color.alpha(pixel) != 0
            else
                pixel != color
        }
        if (firstIndexOfDifferentlyColoredPixel >= 0)
            break
        ++top
    }
    //this is to handle case that entire bitmap is of the same color
    if (top == height)
        return null
    var bottom = height - 1
    while (bottom > top) {
        bitmap.getPixels(buffer, 0, width, 0, bottom, width, 1)
        val firstIndexOfDifferentlyColoredPixel: Int = buffer.indexOfFirst { pixel ->
            if (checkFullTransparency)
                Color.alpha(pixel) != 0
            else
                pixel != color
        }
        if (firstIndexOfDifferentlyColoredPixel >= 0)
            break
        --bottom
    }
    //handle left&right
    val heightRemaining = bottom - top + 1
    if (heightRemaining != buffer.size)
        buffer = IntArray(heightRemaining)
    //left
    var left = 0
    while (left < width) {
        bitmap.getPixels(buffer, 0, 1, left, top, 1, heightRemaining)
        val firstIndexOfDifferentlyColoredPixel: Int = buffer.indexOfFirst { pixel ->
            if (checkFullTransparency)
                Color.alpha(pixel) != 0
            else
                pixel != color
        }
        if (firstIndexOfDifferentlyColoredPixel >= 0)
            break
        ++left
    }
    //right
    var right = width-1
    while (right >= left + 1) {
        bitmap.getPixels(buffer, 0, 1, right, top, 1, heightRemaining)
        val firstIndexOfDifferentlyColoredPixel: Int = buffer.indexOfFirst { pixel ->
            if (checkFullTransparency)
                Color.alpha(pixel) != 0
            else
                pixel != color
        }
        if (firstIndexOfDifferentlyColoredPixel >= 0)
            break
        --right
    }
    return Bitmap.createBitmap(bitmap, left, top, right - left+1, bottom - top+1)
}

Alternative, which is more elegant but always goes over all the pixels of the bitmap:

/**removes rows/columns of the same color from the edges
 * @return a Bitmap after the removal, or null if completely of the same color*/
fun cropToRemoveSameColoredEdges2(bitmap: Bitmap, @ColorInt color: Int = Color.TRANSPARENT): Bitmap? {
    val height = bitmap.height
    val width = bitmap.width
    //this is to handle colors that have various values, but are still just same 100% transparent, so it's the same color
    val checkFullTransparency = Color.alpha(color) == 0
    var top = height
    var bottom = -1
    var left = width
    var right = -1
    val buffer = IntArray(width)
    //go over each pixel of each row
    for (y in 0 until height) {
        bitmap.getPixels(buffer, 0, width, 0, y, width, 1)
        for (x in 0 until width) {
            val pixel = buffer[x]
            val isPixelDifferentFromGivenColor = if (checkFullTransparency)
                Color.alpha(pixel) != 0
            else
                pixel != color
            if (isPixelDifferentFromGivenColor) {
                if (left > x)
                    left = x
                if (right < x)
                    right = x
                if (top > y)
                    top = y
                if (bottom < y)
                    bottom = y
            }
        }
    }
    //not found any pixel that's different from the given color. technically can test all other variables too, but no need
    if(top==height)
        return null
    return Bitmap.createBitmap(bitmap, left, top, right - left + 1, bottom - top + 1)
}
Ancylostomiasis answered 1/6 at 8:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.