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

в 12:38, , рубрики: android, crop, exif, orientation, resize, изображения, Разработка под android, метки: , , , , ,

Многие уже наверняка сталкивались с проблемой 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;
            }
        }
    }
}

Автор: bdiang

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js