Многие уже наверняка сталкивались с проблемой 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();
}
}
Надеюсь сей мануал окажется кому нибудь не только полезным, но и даст понимание. Ибо бездумный копипаст может решить проблему в краткосрочном периоде, но в долгосрочном может привести к еще большим ошибкам.
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;
}
}
}
}
Автор: bdiang