Android Open Source - Android-ImageManager Bitmap Processor






From Project

Back to project page Android-ImageManager.

License

The source code is released under:

Copyright (c) 2011 Felipe Lima Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Softwa...

If you think the Android project Android-ImageManager listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.felipecsl.android.imaging;
/*from   ww w  .  ja  va2 s .  c o m*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;

import com.felipecsl.android.Utils;

public class BitmapProcessor {
    private static final String TAG = "BitmapProcessor";

    /**
     * Method to calculate SampleSize (used in calculateSampleSize)
     */
    public enum SamplingMethod {
        /**
         * Choose the smallest ratio as inSampleSize value, this will guarantee a final image
         * with both dimensions larger than or equal to the requested height and width.
         * http://developer.android.com/training/displaying-bitmaps/load-bitmap.html#load-bitmap
         */
        STANDARD,

        /**
         * Choose the largest ratio as inSampleSize value, this will guarantee a final image
         * with both dimensions smaller than or equal to the requested height and width.
         */
        WITHINREQ,
    };

    private final Context context;

    public BitmapProcessor(final Context context) {
        this.context = context;
    }

    public Bitmap getRoundedCorners(final Bitmap bitmap, final int radius) {
        Bitmap output = null;
        try {
            output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_4444);
        } catch (final OutOfMemoryError e) {
            Log.e(TAG, "Out of memory in getRoundedCorners()");
            return null;
        }
        final Canvas canvas = new Canvas(output);

        final int color = 0xff424242;
        final Paint paint = new Paint();
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        final RectF rectF = new RectF(rect);

        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        canvas.drawRoundRect(rectF, radius, radius, paint);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        canvas.drawBitmap(bitmap, rect, rect, paint);

        return output;
    }

    public Bitmap getAvatarThumbnail(final Bitmap b) {
        Bitmap finalBitmap;
        if (b.getHeight() > b.getWidth()) {
            finalBitmap = Bitmap.createBitmap(b, 0, (b.getHeight() - b.getWidth()) / 2, b.getWidth(), b.getWidth());
        } else if (b.getHeight() < b.getWidth()) {
            finalBitmap = Bitmap.createBitmap(b, (b.getWidth() - b.getHeight()) / 2, 0, b.getHeight(), b.getHeight());
        } else {
            finalBitmap = b;
        }
        finalBitmap = Bitmap.createScaledBitmap(finalBitmap, Utils.dpToPx(context, 50), Utils.dpToPx(context, 50), true);

        final int radius = (int)(finalBitmap.getHeight() * 0.07f);

        return getRoundedCorners(finalBitmap, radius);
    }

    /**
     * Calculate max sampleSize that fits on the required width and height.
     * 
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @param samplingMethod
     * @see {@linkplain http://developer.android.com/training/displaying-bitmaps/load-bitmap.html}
     * @return sampleSize
     */
    public static int calculateSampleSize(final BitmapFactory.Options options, final int reqWidth, final int reqHeight, final SamplingMethod samplingMethod) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            switch (samplingMethod) {
                case STANDARD: {
                    // Standard method found at http://developer.android.com/training/displaying-bitmaps/load-bitmap.html#load-bitmap
                    // Calculate ratios of height and width to requested height and width
                    final int heightRatio = Math.round((float)height / (float)reqHeight);
                    final int widthRatio = Math.round((float)width / (float)reqWidth);

                    // Choose the smallest ratio as inSampleSize value, this will guarantee
                    // a final image with both dimensions larger than or equal to the
                    // requested height and width.
                    inSampleSize = Math.min(widthRatio, heightRatio);
                    break;
                }
                case WITHINREQ: {
                    // Calculate ratios of height and width to requested height and width
                    final float heightRatio = (float)height / (float)reqHeight;
                    final float widthRatio = (float)width / (float)reqWidth;

                    // Choose the largest ratio as inSampleSize value, this will guarantee
                    // a final image with both dimensions smaller than or equal to the
                    // requested height and width.
                    inSampleSize = (int)Math.ceil(Math.max(widthRatio, heightRatio));
                    break;
                }
            }
        }

        return inSampleSize;
    }

    public Bitmap getCircle(final Bitmap bitmap) {
        Bitmap output = null;
        try {
            output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_4444);
        } catch (final OutOfMemoryError e) {
            Log.e(TAG, "Out of memory in getCircle()");
            return null;
        }
        final Canvas canvas = new Canvas(output);

        final int color = 0xff424242;
        final Paint paint = new Paint();
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());

        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);

        return output;
    }

    /**
     * Calculate max sampleSize logarithmically to obtain a ^2 sample
     * <p>
     * TODO: Needs to be tested a bit more (create Unit Test for this)
     * 
     * @param options
     * @param bounds
     * @return sampleSize
     */
    public static int calculateSampleSizeLog(final BitmapFactory.Options options, final int bounds) {
        // Figure out the minimum size to fit within the bounds
        final int ow = options.outWidth, oh = options.outHeight;
        final double delta = ow > bounds ? ow - bounds : oh - bounds;
        final double targetWidth = ow - delta, targetHeight = oh - delta;
        // Figure out max factor sampling within intended bounds
        final boolean scaleByHeight = Math.abs(oh - targetHeight) >= Math.abs(ow - targetWidth);
        final double sampleSize = Math.ceil(scaleByHeight ? oh / targetHeight : ow / targetWidth);
        // Apply logarithm to get the sample closest to its ^2
        final int inSampleSize = (int)Math.pow(2d, Math.floor(Math.log(sampleSize) / Math.log(2d)));

        return inSampleSize;
    }

    /**
     * Write a compressed version of the bitmap to the specified outputstream.
     * If this returns true, the bitmap can be reconstructed by passing a
     * corresponding inputstream to BitmapFactory.decodeStream(). Note: not
     * all Formats support all bitmap configs directly, so it is possible that
     * the returned bitmap from BitmapFactory could be in a different bitdepth,
     * and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque
     * pixels).
     * 
     * @param bitmap Bitmap to compress
     * @param format The format of the compressed image
     * @param quality Hint to the compressor, 0-100. 0 meaning compress for
     *            small size, 100 meaning compress for max quality. Some
     *            formats, like PNG which is lossless, will ignore the
     *            quality setting
     * @return ByteArrayInputStream with compressed bitmap
     */
    public static ByteArrayInputStream compress(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality) {
        try {
            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            bitmap.compress(format, quality, outputStream);
            final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            outputStream.close();
            return inputStream;
        } catch (final IOException e) {
            Log.e(TAG, "compress", e);
            return null;
        }
    }

    public static Bitmap decodeStream(final InputStream stream) {
        try {
            return BitmapFactory.decodeStream(stream);
        } catch (final OutOfMemoryError e) {
            Log.e(TAG, "Out of memory in decodeStream()", e);
            return null;
        }
    }

    public static void decodeSampledBitmapFromRemoteUrl(final Context context,
                                                        final String urlString,
                                                        final int reqWidth,
                                                        final int reqHeight,
                                                        final ImageManagerCallback callback) {

        decodeSampledBitmapFromRemoteUrl(context, urlString, reqWidth, reqHeight, SamplingMethod.STANDARD, callback);
    }

    /**
     * Decodes a sampled Bitmap from the provided url in the requested width and height
     * 
     * @param urlString URL to download the bitmap from
     * @param reqWidth Requested width
     * @param reqHeight Requested height
     */
    public static void decodeSampledBitmapFromRemoteUrl(final Context context,
                                                        final String urlString,
                                                        final int reqWidth,
                                                        final int reqHeight,
                                                        final SamplingMethod samplingMethod,
                                                        final ImageManagerCallback callback) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        options.inPurgeable = true;
        options.inDither = false;
        options.inInputShareable = true;

        byte[] binaryData = BitmapHttpClient.get(urlString);

        if (binaryData == null) {
            callback.onLoadFailed(LoadedFrom.NETWORK, new Exception("binaryData == null"));
            return;
        }

        decodeByteArray(binaryData, options);

        int width = reqWidth;
        int height = reqHeight;

        if (reqWidth == 0) {
            width = options.outWidth;
        }
        if (reqHeight == 0) {
            height = options.outHeight;
        }

        // Calculate inSampleSize
        options.inSampleSize = calculateSampleSize(options, width, height, samplingMethod);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;

        binaryData = BitmapHttpClient.get(urlString);

        if (ImageManager.LOG_CACHE_OPERATIONS)
            Log.d(TAG, "Image downloaded: " + urlString);

        if (binaryData != null)
            callback.onBitmapLoaded(decodeByteArray(binaryData, options), LoadedFrom.NETWORK);
    }

    public static void decodeSampledBitmapFromLocalUri(final Context context,
                                                       final Uri uri,
                                                       final int reqWidth,
                                                       final int reqHeight,
                                                       final ImageManagerCallback callback) {

        decodeSampledBitmapFromLocalUri(context, uri, reqWidth, reqHeight, SamplingMethod.STANDARD, callback);
    }

    /**
     * Calculates sampling to fit reqWidth and reqHeight then returns the sampled Bitmap
     * <p>
     * Can't guarantee Bitmap will be the exact width/height provided. Only that it'll be larger or equal to the requested dimensions
     * 
     * @param context
     * @param Uri needed to open the input stream
     * @param reqWidth requested width
     * @param reqHeight requested height
     * @param SamplingMethod
     * @return Bitmap
     */
    public static void decodeSampledBitmapFromLocalUri(final Context context,
                                                       final Uri uri,
                                                       final int reqWidth,
                                                       final int reqHeight,
                                                       final SamplingMethod samplingMethod,
                                                       final ImageManagerCallback callback) {

        final String contentPath = Utils.getContentPathFromUri(context, uri);

        new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(final Void... params) {
                return decodeSampledBitmapFromLocalUri(context, uri, reqWidth, reqHeight, samplingMethod, contentPath);
            }

            @Override
            protected void onPostExecute(final Bitmap bitmap) {
                if (bitmap != null) {
                    callback.onBitmapLoaded(bitmap, LoadedFrom.LOCAL);
                } else {
                    callback.onLoadFailed(LoadedFrom.LOCAL, new Exception("Failed to load local image"));
                }
            }
        }.execute();
    }

    public static Bitmap decodeSampledBitmapFromLocalUri(final Context context,
                                                         final Uri uri,
                                                         final int reqWidth,
                                                         final int reqHeight,
                                                         final SamplingMethod samplingMethod,
                                                         final String contentPath) {
        InputStream inputStream = null;
        Bitmap bitmap = null;

        try {
            inputStream = context.getContentResolver().openInputStream(uri);

            if (inputStream == null)
                return null;

            final BitmapFactory.Options factoryOptions = decodeBitmapBounds(inputStream);
            factoryOptions.inSampleSize = calculateSampleSize(factoryOptions, reqWidth, reqHeight, samplingMethod);
            factoryOptions.inJustDecodeBounds = false;

            inputStream.close();
            inputStream = context.getContentResolver().openInputStream(uri);
            bitmap = BitmapFactory.decodeStream(inputStream, null, factoryOptions);

            // @note This usually means the Stream is invalid, but recent developments have
            // made me believe there may be instances where this happens due to OOM and
            // BitmapFactory is failing to throw the proper OutOfMemoryError exception.
            if (bitmap == null) {
                Log.e(TAG, "decodeUriWithSampling: decodeStream returned null Bitmap!");
            }

            if (ImageUtil.isJPG(contentPath)) {
                final Bitmap rotatedBitmap = ImageUtil.rotateBitmapToExifOrientation(context, contentPath, bitmap);

                if (rotatedBitmap == null)
                    Log.e(TAG, "decodeUriWithSampling: rotateBitmapToExifOrientation returned null Bitmap!");
                else
                    bitmap = rotatedBitmap;
            }

        } catch (final OutOfMemoryError e) {
            // Auto-downscale on OutOfMemoryError unless we got to an extremely small pixel bound
            if (reqWidth < 256 || reqHeight < 256)
                Log.e(TAG, "Out of memory in decodeUriWithSampling(...)", e);
            else {
                // Yield current Thread to prevent the OS from considering us unresponsive.
                Thread.yield();
                final int newWidth = (int)(reqWidth * .75);
                final int newHeight = (int)(reqHeight * .75);
                Log.w(TAG, "OutOfMemoryError in decodeUriWithSampling(...) trying with " + newWidth + "x" + newHeight);
                bitmap = decodeSampledBitmapFromLocalUri(context, uri, newWidth, newHeight, samplingMethod, contentPath);
            }

        } catch (final FileNotFoundException e) {
            Log.e(TAG, "File not found in decodeUriWithSampling()", e);

        } catch (final IOException e) {
            Log.e(TAG, "IOException in decodeUriWithSampling()", e);

        } catch (final Exception e) {
            Log.e(TAG, "Exception in decodeUriWithSampling()", e);

        } finally {
            try {
                if (inputStream != null)
                    inputStream.close();
            } catch (final IOException e) {}
        }

        return bitmap;
    }

    public static Bitmap decodeByteArray(final byte[] data, final BitmapFactory.Options options) {
        try {
            return BitmapFactory.decodeByteArray(data, 0, data.length, options);
        } catch (final OutOfMemoryError e) {
            Log.e(TAG, "Out of memory in decodeByteArray()", e);
            return null;
        }
    }

    /**
     * Decodes Bitmap metadata without placing the actual raw bitmap in memory.
     * 
     * @param Context
     * @param Uri
     * @return BitmapFactory.Options with metadata
     */
    public static BitmapFactory.Options decodeBitmapBounds(final Context context, final Uri imageUri) {
        try {
            return decodeBitmapBounds(context.getContentResolver().openInputStream(imageUri));
        } catch (final FileNotFoundException e) {
            Log.e(TAG, "decodeBitmapBounds(Context, Uri)", e);
            return null;
        }
    }

    /**
     * Decodes Bitmap metadata without placing the actual raw bitmap in memory.
     * <p>
     * If InputStream doesn't support mark/reset, it'll get closed afterwards.
     * 
     * @param inputStream
     * @return BitmapFactory.Options with metadata
     */
    public static BitmapFactory.Options decodeBitmapBounds(final InputStream inputStream) {
        try {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true; // Only decode metadata, do not place bitmap in memory
            if (inputStream.markSupported()) {
                inputStream.mark(inputStream.available());
                BitmapFactory.decodeStream(inputStream, null, options);
                inputStream.reset();
            } else {
                BitmapFactory.decodeStream(inputStream, null, options);
                inputStream.close();
            }

            return options;

        } catch (final IOException e) {
            Log.e(TAG, "decodeBitmapBounds()", e);
            return null;
        }
    }

    public static InputStream compressToFit(final Context context, Bitmap bitmap, final int maximumSizeInBytes, final int pixelBounds,
                                            final CompressFormat format, final int quality) {
        InputStream inputStream = null;

        try {
            // If the bitmap is within our size bounds, try an initial compression
            if (bitmap.getWidth() <= pixelBounds && bitmap.getHeight() <= pixelBounds)
                inputStream = BitmapProcessor.compress(bitmap, format, quality);

            // Re-scale image if width > widthBound
            float ceil = (float)Math.ceil(pixelBounds * 1.333);
            int rWidth = bitmap.getWidth();
            int rHeight = bitmap.getHeight();

            // If inputStream is null (too big) or the compressed bitmap is too large, downscale
            if (inputStream == null || inputStream.available() > maximumSizeInBytes) {
                Log.i(TAG, "Image is too large" + (inputStream != null ? (" (" + inputStream.available() / 1024 + "kb)") : "") +
                           ", will be adjusted to " + pixelBounds + ", " + pixelBounds);

                do {
                    // Adjust our ceiling
                    ceil = (float)Math.ceil(ceil * .75f);
                    final int fCeil = (int)ceil;

                    Log.d(TAG, "compression attempt at " + fCeil + "x" + fCeil);

                    // Downscale bitmap
                    JobOptions options = new JobOptions();
                    options.requestedWidth = fCeil;
                    options.requestedHeight = fCeil;
                    options.scaleType = ScaleType.FIT_CENTER;
                    Bitmap transformedBitmap = ImageUtil.transformBitmap(bitmap, options);

                    // Store some data we need to access beyond the do{} block
                    rWidth = transformedBitmap.getWidth();
                    rHeight = transformedBitmap.getHeight();

                    // If it's the same, something went wrong (out of mem), iterate here and try a smaller image
                    if (transformedBitmap.equals(bitmap))
                        continue;

                    // Recycle the Bitmap
                    bitmap.recycle();
                    bitmap = null;

                    // Compress to JPEG and get its input stream for uploading
                    if (inputStream != null)
                        inputStream.close();
                    inputStream = BitmapProcessor.compress(transformedBitmap, format, quality);

                    // Lets use the transformedBitmap as our next source
                    bitmap = transformedBitmap;
                    transformedBitmap = null;

                    // Yield to prevent the OS from killing us
                    Thread.yield();

                } while (inputStream.available() > maximumSizeInBytes);
            }

            Log.d(TAG, String.format("Resulting image of size: %d, %d - %dkb", rWidth, rHeight, inputStream != null ? (inputStream.available() / 1024) : 0));

        } catch (final Exception e) { // IOException
            Log.e(TAG, "", e);

        } finally {
            if (bitmap != null)
                bitmap.recycle();
        }

        return inputStream;
    }

    public static InputStream compressToFit(final Context context, final Uri imageUri, final int maximumSizeInBytes, final int pixelBounds) {
        return compressToFit(context, imageUri, maximumSizeInBytes, pixelBounds, CompressFormat.JPEG, 80);
    }

    /**
     * Shrinks and compresses a Image file (Uri) within the maximum size of bytes and pixels.
     * 
     * @param context
     * @param imageUri
     * @param maximumSizeInBytes
     * @param pixelBounds
     * @param format
     * @param quality
     * @return InputStream
     */
    public static InputStream compressToFit(final Context context,
                                            final Uri imageUri,
                                            final int maximumSizeInBytes,
                                            final int pixelBounds,
                                            final CompressFormat format,
                                            final int quality) {

        InputStream inputStream = null;
        Bitmap originalBitmap = null;

        try {
            final ContentResolver contentResolver = context.getContentResolver();
            inputStream = contentResolver.openInputStream(imageUri);
            final BitmapFactory.Options options = BitmapProcessor.decodeBitmapBounds(contentResolver.openInputStream(imageUri));

            Log.i(TAG, String.format("compressToFit: Image dimensions: %dx%d | %dkb", options.outWidth, options.outHeight, inputStream.available() / 1024));

            if (options.outWidth > pixelBounds || options.outHeight > pixelBounds || inputStream.available() > maximumSizeInBytes) {
                final String contentPath = Utils.getContentPathFromUri(context, imageUri);
                originalBitmap = decodeSampledBitmapFromLocalUri(context, imageUri, pixelBounds, pixelBounds, SamplingMethod.STANDARD, contentPath);
                inputStream.close();
                inputStream = BitmapProcessor.compressToFit(context, originalBitmap, maximumSizeInBytes, pixelBounds, format, quality);
            }

        } catch (final FileNotFoundException e) {
            Log.e(TAG, "", e);

        } catch (final IOException e) {
            Log.e(TAG, "", e);

        } finally {
            if (originalBitmap != null)
                originalBitmap.recycle();
        }

        return inputStream;
    }
}




Java Source Code List

com.felipecsl.android.Utils.java
com.felipecsl.android.imaging.BitmapHttpClient.java
com.felipecsl.android.imaging.BitmapProcessor.java
com.felipecsl.android.imaging.CacheManager.java
com.felipecsl.android.imaging.CacheableDrawable.java
com.felipecsl.android.imaging.DiskLruImageCache.java
com.felipecsl.android.imaging.ImageManagerCallback.java
com.felipecsl.android.imaging.ImageManager.java
com.felipecsl.android.imaging.ImageResponseCache.java
com.felipecsl.android.imaging.ImageUtil.java
com.felipecsl.android.imaging.JobOptions.java
com.felipecsl.android.imaging.LoadedFrom.java
com.felipecsl.android.imaging.MemoryLruImageCache.java
com.felipecsl.android.imaging.ProcessorCallback.java
com.felipecsl.android.imaging.ScaleType.java
com.felipecsl.android.imaging.sample.ListAdapter.java
com.felipecsl.android.imaging.sample.MainActivity.java