user782104
user782104

Reputation: 13545

Preserve the image quality when decode stream in android

I have an image on sdcard and need to show on the image view

The problem is that after it has been decoded, it seems the quality is deteriorated. Are there any ways to keep the quality and at the same time preserve the memory?

Or, if I used an larger image, are there any ways to preserve the memory (avoid too large bitmap to load) with scaling? (I need to keep the size of the original image)

Thanks for helping.

public Bitmap decodeFile(String pubKey, int bookPageID, int type)
        throws IOException {
    Bitmap b = null;
    File f = null;
    String uri = null;
    FileInputStream fis = null;

    Log.d(TAG,"pageID to read: " + bookPageID);

    IRIssue issue = Broker.model.issueDataStore.getIRIssue(pubKey);

    String imageFolder = IRConstant.issueFolder(issue.year, issue.month, issue.day, issue.pubKey);

    // pageID - 1 since the page is an array (start at 0) , but page ID start at 1
    if (type == 2){
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].graphicUri;
    }else {
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].textUri;
    }

    f = new File(uri);

    Log.d(TAG,"is file: " + uri + " exist?" + f.exists());

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inJustDecodeBounds = false;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, options);
    fis.close();

    return b;
}

Upvotes: 3

Views: 5282

Answers (6)

Magnus
Magnus

Reputation: 1502

The following code uses several concepts from Displaying Bitmaps Efficiently
First off the bitmap reading is done in a background thread, I'm using mark / reset on inputStream (wrapped with BufferedInputstream) to not read more than necessary from stream when we try to find out the size of the image to use when calculating scale factor. The example code below subsamples the image to match a size of 320x240 pixles. In a non example code one could have simple callback interface send the bitmap from onPostExecute to implementing class (callback interface implementer). Or provide the view as a member tot the AsyncTask directly and set the bitmap in onPostExecute.

Call the code with (example downloaded image on my device):

BitmapTask task = new BitmapTask(getContentResolver());
task.execute(Uri.parse("file:///storage/emulated/0/Download/download.jpg"));

The classes in question

private static class BitmapTask extends AsyncTask<Uri, Void, Bitmap> {

    // prevent mem leaks
    private WeakReference<ContentResolver> mWeakContentResolver;

    public BitmapTask(ContentResolver resolver) {
        mWeakContentResolver = new WeakReference<ContentResolver>(resolver);
    }

    @Override
    protected Bitmap doInBackground(Uri... params) {
        Bitmap bitmap = null;
        ContentResolver resolver = mWeakContentResolver.get();
        if (resolver != null) {
            BufferedInputStream stream = null;
            try {
                stream = new BufferedInputStream(
                        resolver.openInputStream(params[0]));
                stream.mark(1 * 1024);
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = true;
                // Find out size of image
                BitmapFactory.decodeStream(stream, null, options);
                try {
                    stream.reset();
                } catch (IOException e) {
                    Log.d(TAG, "reset failed");
                }
                int imageHeight = options.outHeight;
                int imageWidth = options.outWidth;
                String imageType = options.outMimeType;
                Log.d(TAG, "w, h, mime " + imageWidth + " , " + imageHeight
                        + " , " + imageType);
                options.inJustDecodeBounds = false;
                // Calculate down scale factor
                options.inSampleSize = calculateInSampleSize(options, 320,
                        240);
                return BitmapFactory.decodeStream(stream, null, options);
            } catch (FileNotFoundException e) {
                bitmap = null;
            } finally {
                IOUtils.closeStreamSilently(stream);
            }
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        Log.d(TAG,
                "bitmap result: "
                        + ((result != null) ? "" + result.getByteCount()
                                : "0"));
        result.recycle();
    }
}

public static int calculateInSampleSize(BitmapFactory.Options options,
        int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Edit: For large inputstreams there might be a problem with the mark/reset technique, SkImageDecoder::Factory returned null can be seen in logs sometimes, resulting in null bitmap, se other SO question on the matter: SkImageDecoder::Factory returned null. It can be fixed by reiniting the stream variable again stream = new resolver.openInputStream(params[0])); before return in doInBackground

Edit 2: If you have to preserve the image size but wan't to limit memory usage you could use the options.inPreferredConfig = Bitmap.Config.RGB_565; that halves the memory per pixel, but bare in mind that the images might not have great quality anymore (experiment!).

Upvotes: 3

Dharmendra
Dharmendra

Reputation: 571

GridViewActivity.java

public class GridViewActivity extends Activity implements OnItemClickListener {
    private String[] filepathstring;
    private File[] listfile;
    GridView grid_sdcard;
    File file;
    ImageView image;
    GridViewAdapter adapter;
    int select;
    int sele;



    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.gridview_activity);

        image=(ImageView)convertView.findViewById(R.id.image_show);
        grid_sdcard=(GridView)findViewById(R.id.grid_sdcard);


        if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
        {
        Toast.makeText(this,"Error! No SDCARD Found!", Toast.LENGTH_LONG).show();   
        }
        else
        {
            file=new File(Environment.getExternalStorageDirectory() + File.separator +"eMENU Images/");
            file.mkdirs();
            Toast.makeText(GridViewActivity.this,"Past Image Here:", Toast.LENGTH_LONG).show();
        }
        if(file.isDirectory())
        {
            listfile=file.listFiles();
            for(int i=0;i<listfile.length;i++)
            {
                filepathstring[i]=listfile[i].getAbsolutePath();
            }
        }
        adapter=new GridViewAdapter(GridViewActivity.this,filepathstring);
        grid_sdcard.setAdapter(adapter);
        grid_sdcard.setOnItemClickListener(this);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id1) {
        final String image=filepathstring[position];
        Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
        imageshow.setImageBitmap(bitmap);
    }
}

GridViewAdapter.java

public class GridViewAdapter extends BaseAdapter {
    String[] filepathlist;
    Context context;


    public GridViewAdapter(Context con, String[] filepathstring) {
        context=con;
        filepathlist=filepathstring;
    }

    @Override
    public int getCount() {

        return filepathlist.length;
    }

    @Override
    public Object getItem(int position) {
        // TODO Auto-generated method stub
        return position;
    }

    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        if(convertView==null)
        {
            LayoutInflater inflater=(LayoutInflater)convertView.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView=inflater.inflate(R.layout.griadview_adapter,null);
        }
        ImageView imageshow=(ImageView)convertView.findViewById(R.id.image_show);
        Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
        imageshow.setImageBitmap(bitmap);
        return convertView;
    }
}

Upvotes: 0

Phil
Phil

Reputation: 36289

A fast approach that is also highly configurable is to use a WebView instead of an ImageView:

WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setAllowFileAccess(true);
mWebView.getSettings().setBuiltInZoomControls(true);
String base = Environment.getExternalStorageDirectory().getAbsolutePath().toString();
String imagePath = "file://" + base + "/myImage.png";//replace with the name of the image you are accessing
String html = "<html><head></head><body><img src=\"" + imagePath + "\"></body></html>";
mWebView.loadDataWithBaseURL("", html, "text/html","utf-8", "");

Upvotes: 1

varun bhardwaj
varun bhardwaj

Reputation: 1522

While decoding add options.injustdecodeBounds = true because that shows that you want only bounds not the whole bitmap. This will avoid the memory errors because you will only load the image size you actually require.

Second thing is to scale that bitmap according to your need and to do that without distortion you have to scale it in a way that will maintain the aspect ratio. While clicking picture either you set a fixed ration in which image is clicked and than scale that image in that ratio only. If it is not in your hand than you can use the following method to get the insample size and than will decode the image to a particular size without distortion.

private int calculateSampleSize(int width, int height, int targetWidth, int targetHeight) {
float bitmapWidth = width;
float bitmapHeight = height;

int bitmapResolution = (int) (bitmapWidth * bitmapHeight);
int targetResolution = targetWidth * targetHeight;

int sampleSize = 1;

if (targetResolution == 0) {
    return sampleSize;
}

for (int i = 1; (bitmapResolution / i) > targetResolution; i *= 2) {
    sampleSize = i;
}

return sampleSize;

}

Do provide any feedback if you find any improvements.

Upvotes: 1

Aron Lorincz
Aron Lorincz

Reputation: 1707

BitmapFactory has an inSampleSize property which was designed to solve this problem. Refer to docs: http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize

This article writes about handling bitmaps efficiently: http://developer.android.com/training/displaying-bitmaps/index.html

Upvotes: 1

SalGad
SalGad

Reputation: 3011

I use a custom BitmapHandler class to solve this problem:

public class BitmapHandler {
    private static int IMAGE_MAX_SIZE = 540;  //This can be set to whatever you see fit
    private static String TAG = "BitmapHandler.java";

    public BitmapHandler(Context ctx){
            WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            int width = size.x;
            int height = size.y;
            Log.v(TAG, "Screen width: " + width + " height: " + height);
            IMAGE_MAX_SIZE = (Math.min(width, height))*4; //Try playing with this multiplier number to get different degrees of scaling
    }

    public Bitmap decodeFileAsPath(String uri) {
            // Create a file out of the uri
            File f = null;
            Log.v(TAG, "Incoming uri: " + uri);
            f = new File(uri);

            if (f.equals(null)){
                    Log.v(TAG, "File is null!");
            }
            return decodeFile(f);
    }

    private Bitmap decodeFile(File f) {
            Bitmap b = null;
            try {
                    // Decode image size
                    BitmapFactory.Options o = new BitmapFactory.Options();
                    o.inJustDecodeBounds = true;
                    o.inScaled = false;

                    FileInputStream fis = new FileInputStream(f);
                    BitmapFactory.decodeStream(fis, null, o);
                    fis.close();

                    int scale = 1;
                    Log.v(TAG, "Decode Image height: " + o.outHeight + " and width: " + o.outWidth);

                    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
                            scale = (int) Math.pow(
                                            2,
                                            (int) Math.round(Math.log(IMAGE_MAX_SIZE
                                                            / (double) Math.max(o.outHeight, o.outWidth))
                                                            / Math.log(0.5)));
                    }
                    Log.v(TAG, "Final scale: " + scale);
                    // Decode with inSampleSize
                    BitmapFactory.Options o2 = new BitmapFactory.Options();
                    o2.inScaled = false;
                    o2.inSampleSize = scale;
                    fis = new FileInputStream(f);
                    b = BitmapFactory.decodeStream(fis, null, o2);
                    fis.close();
            } catch (IOException e) {
                    Log.v(TAG, e.getMessage());
            }
            return b;
    }
}

This dynamically scales your image whilst trying to prevent an OutOfMemoryException

Upvotes: 1

Related Questions