📜 ⬆️ ⬇️

Getting the image of the right size without OutOfMemoryError + auto-rotate according to EXIF ​​orientation

Many have probably already faced the problem of OutOfMemoryError and found quite a clever displaying Bitmaps manual Efficiently . But if you have not had time to invent your bicycle on the basis of the manual, I offer my ready-made solution with explanations that can get images:



Usage example
ImageManager im = new ImageManager(ctx, 100, 100); Bitmap bm = im.setIsScale(true) .setIsResize(true) .setIsCrop(true) .getFromFile(myUri.toString()); 


')

OutOfMemoryError


Why does this error occur? The fact is that a limited amount of memory (heap size) is allocated to each application, which varies depending on the device. For example, 16mb, 24mb and higher. Modern devices typically have 24mb and higher, however, these values ​​can be quickly "eaten."

What exactly is memory consuming? The answer lies in the Bitmap class, which in the general case spends 2 or 4 bytes per pixel (depending on the image bit depth - 16bit RGB_555 or 32 bits ARGB_888). Calculate how much a Bitmap will eat, containing an image taken with a 5 megapixel camera.

With a 4: 3 aspect ratio, you will get an image with sides of 2583 x 1936. In the RGB_555 configuration, our Bitmap will take 2583 * 1936 * 2 = 9.54 MB (hereinafter I think that MB = 2 in 20 degrees bytes), and in ARGB_888 in 2 times more - a little more than 19Mb. It is scary to think about cameras with a large number of megapixels.

The solution is short and clear.

1) Using the BitmapFactory.decodeStream function with the passed third parameter new BitmapFactory.Options (), for which inJustDecodeBounds = true, we get a Bitmap containing only the dimensions of the image in pixels and not containing the pixels themselves.
2) Determine how many times you need to reduce the image to get the size we need.
3) Assign this value to the inSampleSize field of the BitmapFactory.Options instance and call the BitmapFactory.decodeStream function again.
4) It is guaranteed that the decoder returns a thumbnail without an OutOfMemoryError

Note: I see no reason to make the size of the image larger than the screen size. Also, I see no reason to store Bitmap in the ARGB_888 configuration, since many devices have 16 bit screens. But even on more colorful screens, the benefit from a two-fold reduction in memory consumption is higher than a slight decrease in image quality (IMHO).

Example
 InputStream in = ... // InputStream BitmapFactory.Options o = new BitmapFactory.Options(); o.inJustDecodeBounds = true; BitmapFactory.decodeStream(in, null, o); in.close(); int origWidth = o.outWidth; //  int origHeight = o.outHeight; //  int bytesPerPixel = 2 // RGB_555  int maxSize = 480 * 800 * bytesPerPixel; //   Bitmap int desiredWidth = …; //  int desiredHeight = …; //  int desiredSize = _ desiredWidth * _ desiredHeight * bytesPerPixel; //   Bitmap   width  height if (desiredSize < maxSize) maxSize = desiredSize; int scale = 1; //  int origSize = origWidth * origHeight * bytesPerPixel; //   if (origWidth > origHeight) { scale = Math.round((float) origHeight / (float) desiredHeight); } else { scale = Math.round((float) origWidth / (float) desiredWidth); } o = new BitmapFactory.Options(); o.inSampleSize = scale; o.inPreferredConfig = Config.RGB_565; in = … // InputStream.  -     , .         InputStream   (  ByteArrayInputStream  FileInputStream). Bitmap bitmap = BitmapFactory.decodeStream(in, null, o); // Bitmap 



What's next?

If you do not need an exact match to the width and height, then the resulting Bitmap is sufficient, otherwise we will resize and / or crop the image. The implementation of these functions is trivial, the source code at the end of the post.

EXIF orientation or correct inverted images.


This solution applies only to the jpeg format.

Guarantees that the objects in the image will always be rotated as we see them - no. It is enough to turn the camera of the smartphone to any angle - and here is an image for you that you don’t use anywhere else. But I want the houses and people to stand on the ground, and the birds flew across the sky. To come to the aid of EXIF ​​is a format that allows you to add additional information to images.

We are only interested in one parameter - orientation, which the smartphone camera adds to the metadata (I want to believe that the cameras of all devices do this). But in its raw form, it stores not a degree of rotation, but a digital value of 1-8. What these meanings are described here . Honestly, I did not memorize what they mean, so I recommend taking the finished function at the end of the post, which converts these values ​​to degrees: getOrientation (Context context, Uri uri). The function returns the values ​​90, 180, 270, 0 or -1 (an erroneous state, when for one reason or another it was not possible to determine the angle of rotation). Accordingly, rotation is required when the value is> 0.

To return the image to the correct angle, you need to supplement the code for obtaining the image:

Instead:

 int origWidth = o.outWidth; //  int origHeight = o.outHeight; //  


We will write:

 int origWidth = 0; //  int origHeight = 0; //  if (orientation == 90 || orientation == 270) { origWidth = o.outHeight; origHeight = o.outWidth; } else { origWidth = o.outWidth; origHeight = o.outHeight; } 


And at the end add:

 if (orientation > 0) { Matrix matrix = new Matrix(); matrix.postRotate(orientation); Bitmap decodedBitmap = bitmap; bitmap = Bitmap.createBitmap(decodedBitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); //     if (decodedBitmap != null && !decodedBitmap.equals(bitmap)) { decodedBitmap.recycle(); } } 


UPDATE 1

We optimize the image


Width and height

The ImageManager class uses two enum ScaleMode and ResizeMode. Each has two values, EQUAL_OR_GREATER and EQUAL_OR_LOWER.

For ScaleMode:


For ResizeMode:


If the image size, memory consumption, space on the SD card, 2g / 3g download speed are not secondary, then it is recommended to use the EQUAL_OR_LOWER mode, which will reduce these parameters due to a slight decrease in image quality. In practice, I quickly came to this compromise, because when sending / receiving images at 2g speed usually leaves much to be desired (especially in an unstable signal, which is not only in tunnels and subways).

Jpeg compression rate

But if ScaleMode and ResizeMode allow saving on width and height, then there is another way to reduce the image size: jpeg compression rate. To do this, in the ImageManager class, I wrote a method and its overload:

 byte[] getRawFromFile(String path, int compressRate) byte[] getRawFromFile(String path) 


In the overload of the method with a hardcode, the parameter empirically derived parameter 75 is hammered. I use this method together with ScaleMode and ResizeMode to get an optimized jpeg image in the form of a byte array, which is the very thing to send it to the post in the server.

Afterword


I hope this manual will prove to someone not only useful, but it will also give you insight. For thoughtless copy-paste can solve the problem in the short term, but in the long term it can lead to even bigger mistakes.

Listing of ImageManager class
 public final class ImageManager { private Context _ctx; private int _boxWidth; private int _boxHeight; private ResizeMode _resizeMode; private ScaleMode _scaleMode; private Config _rgbMode; private boolean _isScale; private boolean _isResize; private boolean _isCrop; private boolean _isRecycleSrcBitmap; private boolean _useOrientation; public ImageManager(Context ctx, int boxWidth, int boxHeight) { this(ctx); _boxWidth = boxWidth; _boxHeight = boxHeight; } public ImageManager(Context ctx) { _ctx = ctx; _isScale = false; _isResize = false; _isCrop = false; _isRecycleSrcBitmap = true; _useOrientation = false; } public ImageManager setResizeMode(ResizeMode mode) { _resizeMode = mode; return this; } public ImageManager setScaleMode(ScaleMode mode) { _scaleMode = mode; return this; } public ImageManager setRgbMode(Config mode) { _rgbMode = mode; return this; } public ImageManager setIsScale(boolean isScale) { _isScale = isScale; return this; } public ImageManager setIsResize(boolean isResize) { _isResize = isResize; return this; } public ImageManager setIsCrop(boolean isCrop) { _isCrop = isCrop; return this; } public ImageManager setUseOrientation(boolean value) { _useOrientation = value; return this; } public ImageManager setIsRecycleSrcBitmap(boolean value) { _isRecycleSrcBitmap = value; return this; } public Bitmap getFromFile(String path) { Uri uri = Uri.parse(path); int orientation = -1; if (_useOrientation) { orientation = getOrientation(_ctx, uri); } Bitmap bitmap = scale(new StreamFromFile(_ctx, path), orientation); return getFromBitmap(bitmap); } public Bitmap getFromBitmap(Bitmap bitmap) { if (bitmap == null) return null; if (_isResize) bitmap = resize(bitmap); if (_isCrop) bitmap = crop(bitmap); return bitmap; } public byte[] getRawFromFile(String path) { return getRawFromFile(path, 75); } public byte[] getRawFromFile(String path, int compressRate) { Bitmap scaledBitmap = getFromFile(path); if (scaledBitmap == null) return null; ByteArrayOutputStream output = new ByteArrayOutputStream(); scaledBitmap.compress(CompressFormat.JPEG, compressRate, output); recycleBitmap(scaledBitmap); byte[] rawImage = output.toByteArray(); if (rawImage == null) { return null; } return rawImage; } public Bitmap getFromByteArray(byte[] rawImage) { Bitmap bitmap = scale(new StreamFromByteArray(rawImage), -1); return getFromBitmap(bitmap); } @SuppressLint("NewApi") private Bitmap scale(IStreamGetter streamGetter, int orientation) { try { InputStream in = streamGetter.Get(); if (in == null) return null; Bitmap bitmap = null; Config rgbMode = _rgbMode != null ? _rgbMode : Config.RGB_565; if (!_isScale) { BitmapFactory.Options o = new BitmapFactory.Options(); o.inPreferredConfig = rgbMode; if (android.os.Build.VERSION.SDK_INT >= 11) { o.inMutable = true; } bitmap = BitmapFactory.decodeStream(in, null, o); in.close(); return bitmap; } if (_boxWidth == 0 || _boxHeight == 0) { if (in != null) in.close(); return null; } ScaleMode scaleMode = _scaleMode != null ? _scaleMode : ScaleMode.EQUAL_OR_GREATER; int bytesPerPixel = rgbMode == Config.ARGB_8888 ? 4 : 2; int maxSize = 480 * 800 * bytesPerPixel; int desiredSize = _boxWidth * _boxHeight * bytesPerPixel; if (desiredSize < maxSize) maxSize = desiredSize; BitmapFactory.Options o = new BitmapFactory.Options(); o.inJustDecodeBounds = true; BitmapFactory.decodeStream(in, null, o); in.close(); int scale = 1; int origWidth; int origHeight; if (orientation == 90 || orientation == 270) { origWidth = o.outHeight; origHeight = o.outWidth; } else { origWidth = o.outWidth; origHeight = o.outHeight; } while ((origWidth * origHeight * bytesPerPixel) * (1 / Math.pow(scale, 2)) > maxSize) { scale++; } if (scaleMode == ScaleMode.EQUAL_OR_LOWER) { scale++; } o = new BitmapFactory.Options(); o.inSampleSize = scale; o.inPreferredConfig = rgbMode; in = streamGetter.Get(); if (in == null) return null; bitmap = BitmapFactory.decodeStream(in, null, o); in.close(); if (orientation > 0) { Matrix matrix = new Matrix(); matrix.postRotate(orientation); Bitmap decodedBitmap = bitmap; bitmap = Bitmap.createBitmap(decodedBitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (decodedBitmap != null && !decodedBitmap.equals(bitmap)) { recycleBitmap(decodedBitmap); } } return bitmap; } catch (IOException e) { return null; } } private Bitmap resize(Bitmap sourceBitmap) { if (sourceBitmap == null) return null; if (_resizeMode == null) _resizeMode = ResizeMode.EQUAL_OR_GREATER; float srcRatio; float boxRatio; int srcWidth = 0; int srcHeight = 0; int resizedWidth = 0; int resizedHeight = 0; srcWidth = sourceBitmap.getWidth(); srcHeight = sourceBitmap.getHeight(); if (_resizeMode == ResizeMode.EQUAL_OR_GREATER && (srcWidth <= _boxWidth || srcHeight <= _boxHeight) || _resizeMode == ResizeMode.EQUAL_OR_LOWER && srcWidth <= _boxWidth && srcHeight <= _boxHeight) { return sourceBitmap; } srcRatio = (float)srcWidth / (float)srcHeight; boxRatio = (float)_boxWidth / (float)_boxHeight; if (srcRatio > boxRatio && _resizeMode == ResizeMode.EQUAL_OR_GREATER || srcRatio < boxRatio && _resizeMode == ResizeMode.EQUAL_OR_LOWER) { resizedHeight = _boxHeight; resizedWidth = (int)((float)resizedHeight * srcRatio); } else { resizedWidth = _boxWidth; resizedHeight = (int)((float)resizedWidth / srcRatio); } Bitmap resizedBitmap = Bitmap.createScaledBitmap(sourceBitmap, resizedWidth, resizedHeight, true); if (_isRecycleSrcBitmap && !sourceBitmap.equals(resizedBitmap)) { recycleBitmap(sourceBitmap); } return resizedBitmap; } private Bitmap crop(Bitmap sourceBitmap) { if (sourceBitmap == null) return null; int srcWidth = sourceBitmap.getWidth(); int srcHeight = sourceBitmap.getHeight(); int croppedX = 0; int croppedY = 0; croppedX = (srcWidth > _boxWidth) ? (int)((srcWidth - _boxWidth) / 2) : 0; croppedY = (srcHeight > _boxHeight) ? (int)((srcHeight - _boxHeight) / 2) : 0; if (croppedX == 0 && croppedY == 0) return sourceBitmap; Bitmap croppedBitmap = null; try { croppedBitmap = Bitmap.createBitmap(sourceBitmap, croppedX, croppedY, _boxWidth, _boxHeight); } catch(Exception e) { } if (_isRecycleSrcBitmap && !sourceBitmap.equals(croppedBitmap)) { recycleBitmap(sourceBitmap); } return croppedBitmap; } public static void recycleBitmap(Bitmap bitmap) { if (bitmap == null || bitmap.isRecycled()) return; bitmap.recycle(); System.gc(); } private static interface IStreamGetter { public InputStream Get(); } private static class StreamFromFile implements IStreamGetter { private String _path; private Context _ctx; public StreamFromFile(Context ctx, String path) { _path = path; _ctx = ctx; } @SuppressWarnings("resource") public InputStream Get() { try { Uri uri = Uri.parse(_path); return "content".equals(uri.getScheme()) ? _ctx.getContentResolver().openInputStream(uri) : new FileInputStream(_path); } catch (FileNotFoundException e) { return null; } } } private static class StreamFromByteArray implements IStreamGetter { private byte[] _rawImage; public StreamFromByteArray(byte[] rawImage) { _rawImage = rawImage; } public InputStream Get() { if (_rawImage == null) return null; return new ByteArrayInputStream(_rawImage); } } private static int getOrientation(Context context, Uri uri) { if ("content".equals(uri.getScheme())) { Cursor cursor = context.getContentResolver().query(uri, new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null); if (cursor == null || cursor.getCount() != 1) { return -1; } cursor.moveToFirst(); int orientation = cursor.getInt(0); cursor.close(); return orientation; } else { try { ExifInterface exif = new ExifInterface(uri.getPath()); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_270: return 270; case ExifInterface.ORIENTATION_ROTATE_180: return 180; case ExifInterface.ORIENTATION_ROTATE_90: return 90; case ExifInterface.ORIENTATION_NORMAL: return 0; default: return -1; } } catch (IOException e) { return -1; } } } } 

Source: https://habr.com/ru/post/161027/


All Articles