Android: rotate image without loading it to memory

Viewed 15123

I am wondering is it possible to rotate an image stored on sdcard without loading it's to memory.

The reason is why I am looking for that is famous OutOfMemoryError. I know I can avoid it by downsampling large image but in fact I don't want to reduce size of that image, I want to have original image but rotated on 90 degrees.

Any suggestions about that are warmly appreciated :)

6 Answers

NOTE: that this answer is really just expanding on making riwnodennyk's answer a bit easier to implement. It's all renderscript and not concerned with anything NDK.

Covers all common rotations and getting Orientation.

To get the rotation you need to use the ExifInterface. Use the one in the support library as there are issues with the sdk version. It works with JPEG and RAW (and similar) Files as this information is embedded in the file (not in the decoded Bitmap).

add to build.gradle

implementation "com.android.support:exifinterface:28.0.0"

Use this if you have handle to the file

import android.renderscript.Allocation;
import android.renderscript.RenderScript;
import your.application.package.rs.ScriptC_rotator;
...

public static Bitmap getCorrectlyRotatedBitmap(@NonNull Context context, @NonNull File imageFile) throws IOException {
    ExifInterface ei = new ExifInterface(imageFile.getPath());
    Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getPath());
    int neededRotationClockwise = ei.getRotationDegrees() % 360;
    return rotateClockwise(context, bitmap, neededRotationClockwise);
}

For the Bitmap rotation itself

public static Bitmap rotateClockwise(@NonNull Context context, @NonNull Bitmap bitmap, int degrees) {
    Log.i(TAG, "rotate bitmap degrees: " + degrees);
    if (degrees == 0F) return bitmap;

    RenderScript rs = RenderScript.create(context);
    ScriptC_rotator script = new ScriptC_rotator(rs);
    script.set_inWidth(bitmap.getWidth());
    script.set_inHeight(bitmap.getHeight());
    Allocation sourceAllocation = Allocation.createFromBitmap(rs, bitmap,
            Allocation.MipmapControl.MIPMAP_NONE,
            Allocation.USAGE_SCRIPT);
    bitmap.recycle();
    script.set_inImage(sourceAllocation);

    Bitmap.Config config = bitmap.getConfig();

    switch (degrees) {
        case 90: {
            Bitmap target = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getWidth(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_90_clockwise(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        case 180: {
            Bitmap target = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_180(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        case 270: {
            Bitmap target = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getWidth(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_270_clockwise(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        default:
            throw new IllegalArgumentException("rotateClockwise() only supports 90 degree increments");
    }

}

And the renderscript, Create file in src/main/rs/rotator.rs, This is the default location that Renderscript is used from, but you'll have to create the directory yourself.

Change the your.application.package.rs as appropriate

#pragma version(1)
#pragma rs java_package_name(your.application.package.rs)

rs_allocation inImage;
int inWidth;
int inHeight;

uchar4 __attribute__ ((kernel)) rotate_270_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX  = inWidth - 1 - y;
    uint32_t inY = x;
    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) rotate_90_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = y;
    uint32_t inY = inHeight - 1 - x;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) rotate_180 (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = inWidth - 1 - x;
    uint32_t inY = inHeight - 1 - y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) flip_vertical (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = x;
    uint32_t inY = inHeight - 1 - y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) flip_horizontal (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = inWidth - 1 - x;
    uint32_t inY = y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

I will suggest you to use some 3rd party library that does not store data on process heap while operating on bitmaps. In my case I used ffmpeg that I was already using for other purpose in my project.

Related