Как масштабировать потоковое растровое изображение на месте, не читая сначала все изображение?

У меня есть приложение для Android, которое очень интенсивно работает с изображениями. В настоящее время я использую Bitmap.createScaledBitmap() для масштабирования изображения до нужного размера. Однако этот метод требует, чтобы у меня уже было оригинальное растровое изображение в памяти, которое может быть довольно значительным.

Как я могу масштабировать растровое изображение, которое загружаю без предварительной записи всей вещи в локальную память или файловую систему?

Ответ 1

Этот метод будет считывать информацию заголовка из изображения для определения его размера, затем читать изображение и масштабировать его до нужного размера на месте без выделения памяти для полного изображения оригинального размера.

Он также использует BitmapFactory.Options.inPurgeable, который, как представляется, является редко документированным, но желательным вариантом для предотвращения исключений OoM при использовании большого количества растровые изображения. UPDATE: больше не использует inPurgeable, см. эту заметку от Romain

Он работает с использованием BufferedInputStream для чтения информации заголовка для изображения перед прочтением всего изображения через InputStream.

/**
 * Read the image from the stream and create a bitmap scaled to the desired
 * size.  Resulting bitmap will be at least as large as the 
 * desired minimum specified dimensions and will keep the image proportions 
 * correct during scaling.
 */
protected Bitmap createScaledBitmapFromStream( InputStream s, int minimumDesiredBitmapWith, int minimumDesiredBitmapHeight ) {

    final BufferedInputStream is = new BufferedInputStream(s, 32*1024);
    try {
        final Options decodeBitmapOptions = new Options();
        // For further memory savings, you may want to consider using this option
        // decodeBitmapOptions.inPreferredConfig = Config.RGB_565; // Uses 2-bytes instead of default 4 per pixel

        if( minimumDesiredBitmapWidth >0 && minimumDesiredBitmapHeight >0 ) {
            final Options decodeBoundsOptions = new Options();
            decodeBoundsOptions.inJustDecodeBounds = true;
            is.mark(32*1024); // 32k is probably overkill, but 8k is insufficient for some jpgs
            BitmapFactory.decodeStream(is,null,decodeBoundsOptions);
            is.reset();

            final int originalWidth = decodeBoundsOptions.outWidth;
            final int originalHeight = decodeBoundsOptions.outHeight;

            // inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
            decodeBitmapOptions.inSampleSize= Math.max(1,Math.min(originalWidth / minimumDesiredBitmapWidth, originalHeight / minimumDesiredBitmapHeight));

        }

        return BitmapFactory.decodeStream(is,null,decodeBitmapOptions);

    } catch( IOException e ) {
        throw new RuntimeException(e); // this shouldn't happen
    } finally {
        try {
            is.close();
        } catch( IOException ignored ) {}
    }

}

Ответ 2

Вот моя версия, основанная на решении @emmby (спасибо человеку!) Я включил вторую фазу, где вы берете уменьшенное растровое изображение и масштабируете его снова, чтобы соответствовать точно вашим желаемым размерам. Моя версия принимает путь к файлу, а не поток.

protected Bitmap createScaledBitmap(String filePath, int desiredBitmapWith, int desiredBitmapHeight) throws IOException, FileNotFoundException {
    BufferedInputStream imageFileStream = new BufferedInputStream(new FileInputStream(filePath));
    try {
        // Phase 1: Get a reduced size image. In this part we will do a rough scale down
        int sampleSize = 1;
        if (desiredBitmapWith > 0 && desiredBitmapHeight > 0) {
            final BitmapFactory.Options decodeBoundsOptions = new BitmapFactory.Options();
            decodeBoundsOptions.inJustDecodeBounds = true;
            imageFileStream.mark(64 * 1024);
            BitmapFactory.decodeStream(imageFileStream, null, decodeBoundsOptions);
            imageFileStream.reset();
            final int originalWidth = decodeBoundsOptions.outWidth;
            final int originalHeight = decodeBoundsOptions.outHeight;
            // inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
            sampleSize = Math.max(1, Math.max(originalWidth / desiredBitmapWith, originalHeight / desiredBitmapHeight));
        }
        BitmapFactory.Options decodeBitmapOptions = new BitmapFactory.Options();
        decodeBitmapOptions.inSampleSize = sampleSize;
        decodeBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565; // Uses 2-bytes instead of default 4 per pixel

        // Get the roughly scaled-down image
        Bitmap bmp = BitmapFactory.decodeStream(imageFileStream, null, decodeBitmapOptions);

        // Phase 2: Get an exact-size image - no dimension will exceed the desired value
        float ratio = Math.min((float)desiredBitmapWith/ (float)bmp.getWidth(), (float)desiredBitmapHeight/ (float)bmp.getHeight());
        int w =(int) ((float)bmp.getWidth() * ratio);
        int h =(int) ((float)bmp.getHeight() * ratio);
        return Bitmap.createScaledBitmap(bmp, w,h, true);

    } catch (IOException e) {
        throw e;
    } finally {
        try {
            imageFileStream.close();
        } catch (IOException ignored) {
        }
    }
}