Получение изображения нужного размера без OutOfMemoryError + автоповорот согласно EXIF orientation

от автора

Многие уже наверняка сталкивались с проблемой OutOfMemoryError и находили достаточно толковый мануал Displaying Bitmaps Efficiently. Но если вы еще не успели изобрести свой велосипед на основе мануала, предлагаю свое готовое решение с объяснениями, которое умеет получать изображения:

  • Bitmap, byte[]
  • С сохранением пропорций
  • C обрезанием краев (crop) до заданного размера width x height
  • Учитывает EXIF orientation, чтобы изображение на выходе всегда было правильно повернуто

OutOfMemoryError

Почему происходит эта ошибка? Все дело в том, что на каждое приложение выделяется ограниченное количество памяти (heap size), разное в зависимости от устройства. Например, 16мб, 24мб и выше. Современные устройства как правило имеют 24мб и выше, однако и эти величины можно быстро «съесть».

Что же именно поглощает память? Ответ кроется в классе Bitmap, который на каждый пиксел тратит в общем случае 2 или 4 байта (зависит от битности изображения – 16бит RGB_555 или 32 бита ARGB_888). Посчитаем сколько съест Bitmap, содержащий изображение, снятое на 5 мегапиксельную камеру.

При соотношении сторон 4:3 получится изображение со сторонами 2583 х 1936. В RGB_555 конфигурации наш Bitmap займет 2583 * 1936 * 2 = 9.54Мб (здесь и далее считаю, что Мб = 2 в 20 степени байт), а в ARGB_888 в 2 раза больше – чуть более 19Мб. Про камеры с большим количеством мегапикселей подумать страшно.

Решение коротко и ясно.

1) Используя функцию BitmapFactory.decodeStream с переданным третьим параметром new BitmapFactory.Options(), у которого inJustDecodeBounds = true получаем Bitmap содержащий только размеры изображения в пикселах и не содержащий самих пикселов.
2) Определяем во сколько раз нужно уменьшить изображение, чтобы получить нужные нам размеры.
3) Присваеваем это значение в поле inSampleSize инстанса BitmapFactory.Options и снова вызываем функцию BitmapFactory.decodeStream.
4) Гарантируется, что декодер вернет уменьшенное изображение без OutOfMemoryError

Примечание: Не вижу смысла делать размер изображения больше чем размер экрана. Также не вижу смысла хранить Bitmap в конфигурации ARGB_888, поскольку многие девайсы имеют 16 битные экраны. Но даже и на более цветастых экранах выгода от двукратного уменьшения потребляемой памяти выше, чем незначительное снижение качества изображения (ИМХО).

Пример

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 

Что дальше?

Если точное соответствие ширине и высоте вам не требуется, то полученного Bitmap’а достаточно, иначе ресайзим и/или обрезаем изображение. Реализация этих функций тривиальна, исходные коды в конце поста.

EXIF orientation или исправляем перевернутые изображения.

Данное решение применимо только к формату jpeg.

Гарантии, что предметы на изображении всегда будут повернуты так, как мы их видим – нет. Достаточно повернуть камеру смартфона на любой угол – и вот вам изображение, которое особо нигде не используешь. Но хочется, чтобы дома и люди стояли на земле, а птицы летели по небу. На помощь приходить EXIF – формат, позволяющий добавлять дополнительную информацию к изображениям.

Нас интересует лишь один параметр – orientation. Но в сыром виде он хранит не градус поворота, а цифровое значение 1-8. Что означают эти значения, описано здесь. Честно говоря, я не стал заучивать, что они означают, поэтому рекомендую взять готовую функцию в конце поста перевода этих значений в градусы: getOrientation(Context context, Uri uri). Функция возвращает значения 90, 180, 270 или -1 (означает, что поворот не требуется).

Чтобы вернуть изображение в правильный ракурс, нужно дополнить код по получению изображения:

Вместо:

int origWidth = o.outWidth; //исходная ширина  int origHeight = o.outHeight; //исходная высота 

Напишем:

int origWidth = 0; //исходная ширина  int origHeight = 0; //исходная высота if (orientation == 90 || orientation == 270) {   origWidth = o.outHeight;   origHeight = o.outWidth; } else {   origWidth = o.outWidth;   origHeight = o.outHeight;  } 

А в конце добавим:

            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();                 }             } 

Надеюсь сей мануал окажется кому нибудь не только полезным, но и даст понимание. Ибо бездумный копипаст может решить проблему в краткосрочном периоде, но в долгосрочном может привести к еще большим ошибкам.

Исходный код класса ImageManager

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;                     default:                         return -1;                 }             } catch (IOException e) {                 return -1;             }         }     } } 

ссылка на оригинал статьи http://habrahabr.ru/post/161027/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *